diff --git a/.github/workflows/interop-test.yml b/.github/workflows/interop-test.yml index 5d97ba3058..8c70c23a6a 100644 --- a/.github/workflows/interop-test.yml +++ b/.github/workflows/interop-test.yml @@ -12,11 +12,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ipfs/aegir/actions/cache-node-modules@master - with: - directories: | - ./interop/node_modules - - name: Build interop - run: (cd interop && npm i && npm run build) - name: Build images run: (cd interop && make) - name: Save package-lock.json as artifact diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c18845b81e..ba70b783b5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -102,7 +102,7 @@ jobs: test-webkit: needs: check - runs-on: ubuntu-latest + runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -116,6 +116,22 @@ jobs: directory: ./.nyc_output flags: webkit + test-webkit-webworker: + needs: check + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx playwright install-deps + - run: npm run --if-present test:webkit-webworker + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: webkit-webworker + test-electron-main: needs: check runs-on: ubuntu-latest @@ -165,6 +181,8 @@ jobs: test-chrome-webworker, test-firefox, test-firefox-webworker, + test-webkit, + test-webkit-webworker, test-electron-main, test-electron-renderer, test-interop diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 15cb8a7cf1..ce2dc358a3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1,26 @@ -{"packages/interface-address-manager":"3.0.1","packages/interface-compliance-tests":"3.0.7","packages/interface-connection":"5.1.1","packages/interface-connection-compliance-tests":"2.0.9","packages/interface-connection-encrypter":"4.0.1","packages/interface-connection-encrypter-compliance-tests":"5.0.1","packages/interface-connection-gater":"3.0.1","packages/interface-connection-manager":"3.0.1","packages/interface-content-routing":"2.1.1","packages/interface-dht":"2.0.3","packages/interface-keychain":"2.0.5","packages/interface-keys":"1.0.8","packages/interface-libp2p":"3.2.0","packages/interface-metrics":"4.0.8","packages/interface-mocks":"12.0.1","packages/interface-peer-discovery":"2.0.0","packages/interface-peer-discovery-compliance-tests":"2.0.8","packages/interface-peer-id":"2.0.2","packages/interface-peer-info":"1.0.10","packages/interface-peer-routing":"1.1.1","packages/interface-peer-store":"2.0.4","packages/interface-pubsub":"4.0.1","packages/interface-pubsub-compliance-tests":"5.0.9","packages/interface-record":"2.0.7","packages/interface-record-compliance-tests":"2.0.5","packages/interface-registrar":"2.0.12","packages/interface-stream-muxer":"4.1.2","packages/interface-stream-muxer-compliance-tests":"7.0.3","packages/interface-transport":"4.0.3","packages/interface-transport-compliance-tests":"4.0.2","packages/interfaces":"3.3.2","packages/libp2p":"0.45.9"} \ No newline at end of file +{ + "packages/peer-discovery-bootstrap": "8.0.0", + "packages/crypto": "1.0.17", + "packages/interface-address-manager":"3.0.1","packages/interface-compliance-tests":"3.0.7","packages/interface-connection":"5.1.1","packages/interface-connection-compliance-tests":"2.0.9","packages/interface-connection-encrypter":"4.0.1","packages/interface-connection-encrypter-compliance-tests":"5.0.1","packages/interface-connection-gater":"3.0.1","packages/interface-connection-manager":"3.0.1","packages/interface-content-routing":"2.1.1","packages/interface-dht":"2.0.3","packages/interface-keychain":"2.0.5","packages/interface-keys":"1.0.8","packages/interface-libp2p":"3.2.0","packages/interface-metrics":"4.0.8","packages/interface-mocks":"12.0.1","packages/interface-peer-discovery":"2.0.0","packages/interface-peer-discovery-compliance-tests":"2.0.8","packages/interface-peer-id":"2.0.2","packages/interface-peer-info":"1.0.10","packages/interface-peer-routing":"1.1.1","packages/interface-peer-store":"2.0.4","packages/interface-pubsub":"4.0.1","packages/interface-pubsub-compliance-tests":"5.0.9","packages/interface-record":"2.0.7","packages/interface-record-compliance-tests":"2.0.5","packages/interface-registrar":"2.0.12","packages/interface-stream-muxer":"4.1.2","packages/interface-stream-muxer-compliance-tests":"7.0.3","packages/interface-transport":"4.0.3","packages/interface-transport-compliance-tests":"4.0.2","packages/interfaces":"3.3.2", + "packages/kad-dht": "9.3.6", + "packages/keychain": "2.0.1", + "packages/libp2p":"0.45.9", + "packages/logger":"2.1.1", + "packages/peer-discovery-mdns":"8.0.0", + "packages/stream-multiplexer-mplex":"8.0.4", + "packages/multistream-select":"3.1.9", + "packages/peer-collections":"3.0.2", + "packages/peer-id":"2.0.3", + "packages/peer-id-factory":"2.0.3", + "packages/peer-record":"5.0.4", + "packages/peer-store":"8.2.1", + "packages/metrics-prometheus":"1.1.5", + "packages/record":"3.0.4", + "packages/transport-tcp":"7.0.3", + "packages/topology":"4.0.3", + "packages/tracked-map":"3.0.3", + "packages/utils": "3.0.12", + "packages/transport-webrtc":"2.0.10", + "packages/transport-websockets":"6.0.3", + "packages/transport-webtransport":"2.0.2" +} diff --git a/.release-please.json b/.release-please.json index 83d6cd719d..9e3c14c9a1 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": { + "packages/crypto": {}, "packages/interface-address-manager": {}, "packages/interface-compliance-tests": {}, "packages/interface-connection": {}, @@ -35,6 +36,27 @@ "packages/interface-transport": {}, "packages/interface-transport-compliance-tests": {}, "packages/interfaces": {}, - "packages/libp2p": {} + "packages/kad-dht": {}, + "packages/keychain": {}, + "packages/libp2p": {}, + "packages/logger": {}, + "packages/metrics-prometheus": {}, + "packages/multistream-select": {}, + "packages/peer-collections": {}, + "packages/peer-discovery-bootstrap": {}, + "packages/peer-discovery-mdns": {}, + "packages/peer-id": {}, + "packages/peer-id-factory": {}, + "packages/peer-record": {}, + "packages/peer-store": {}, + "packages/record": {}, + "packages/stream-multiplexer-mplex": {}, + "packages/topology": {}, + "packages/tracked-map": {}, + "packages/transport-tcp": {}, + "packages/transport-webrtc": {}, + "packages/transport-websockets": {}, + "packages/transport-webtransport": {}, + "packages/utils": {} } } diff --git a/interop/BrowserDockerfile b/interop/BrowserDockerfile index aa6f0077be..55b8613ce1 100644 --- a/interop/BrowserDockerfile +++ b/interop/BrowserDockerfile @@ -4,7 +4,7 @@ FROM mcr.microsoft.com/playwright COPY --from=node-js-libp2p-head /app/ /app/ WORKDIR /app/interop -RUN ./node_modules/.bin/playwright install +RUN npx playwright install ARG BROWSER=chromium # Options: chromium, firefox, webkit ENV BROWSER=$BROWSER diff --git a/interop/Dockerfile b/interop/Dockerfile index 8f981e0aa1..9d28f0229e 100644 --- a/interop/Dockerfile +++ b/interop/Dockerfile @@ -3,13 +3,9 @@ FROM node:18 WORKDIR /app COPY package.json . COPY ./node_modules ./node_modules -COPY ./packages/libp2p ./packages/libp2p +COPY ./packages ./packages +COPY ./interop ./interop WORKDIR /app/interop -COPY ./interop/node_modules ./node_modules -COPY ./interop/dist ./dist -COPY ./interop/package.json . -COPY ./interop/.aegir.js . -COPY ./interop/relay.js . ENTRYPOINT [ "npm", "test", "--", "--build", "false", "--types", "false", "-t", "node" ] diff --git a/interop/LICENSE b/interop/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/interop/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/interop/LICENSE-APACHE b/interop/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/interop/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/interop/LICENSE-MIT b/interop/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/interop/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/interop/README.md b/interop/README.md new file mode 100644 index 0000000000..b4d301f10f --- /dev/null +++ b/interop/README.md @@ -0,0 +1,36 @@ +# @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) + +> Multidimensional Interop Test + +## Table of contents + +- [Install](#install) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/multidim-interop +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/interop/package.json b/interop/package.json index 78108d265b..896306d3bf 100644 --- a/interop/package.json +++ b/interop/package.json @@ -1,34 +1,60 @@ { - "name": "multidim-interop", - "private": true, + "name": "@libp2p/multidim-interop", "version": "1.0.0", - "description": "Multidimension Interop Test", - "type": "module", - "main": "index.js", + "description": "Multidimensional Interop Test", "author": "Glen De Cauwsemaecker / @marcopolo", - "license": "MIT", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/interop#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, "scripts": { "start": "node index.js", "build": "aegir build", "test": "aegir test" }, "dependencies": { - "@chainsafe/libp2p-noise": "^12.0.0", + "@chainsafe/libp2p-noise": "^12.0.1", "@chainsafe/libp2p-yamux": "^4.0.1", - "@libp2p/mplex": "^8.0.1", - "@libp2p/tcp": "^7.0.1", - "@libp2p/webrtc": "^2.0.7", - "@libp2p/websockets": "^6.0.1", - "@libp2p/webtransport": "^2.0.1", + "@libp2p/mplex": "^8.0.0", + "@libp2p/tcp": "^7.0.0", + "@libp2p/webrtc": "^2.0.0", + "@libp2p/websockets": "^6.0.0", + "@libp2p/webtransport": "^2.0.0", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.3", - "libp2p": "../packages/libp2p", + "libp2p": "^0.45.0", "redis": "4.5.1" }, + "devDependencies": { + "aegir": "^39.0.10" + }, "browser": { "@libp2p/tcp": false }, - "devDependencies": { - "aegir": "^39.0.5" - } + "private": true } diff --git a/interop/test/ping.spec.ts b/interop/test/ping.spec.ts index 391c75745b..083ad7df81 100644 --- a/interop/test/ping.spec.ts +++ b/interop/test/ping.spec.ts @@ -46,6 +46,12 @@ describe('ping test', () => { services: { ping: pingService(), identify: identifyService() + }, + connectionManager: { + // disable auto-dial + minConnections: 0, + // slow CI is slow + dialTimeout: 60 * 1000 * 5 } } diff --git a/interop/tsconfig.json b/interop/tsconfig.json index 55b334a3e5..5be3797bc7 100644 --- a/interop/tsconfig.json +++ b/interop/tsconfig.json @@ -1,10 +1,30 @@ { - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ] -} \ No newline at end of file + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../packages/libp2p" + }, + { + "path": "../packages/stream-multiplexer-mplex" + }, + { + "path": "../packages/transport-tcp" + }, + { + "path": "../packages/transport-webrtc" + }, + { + "path": "../packages/transport-websockets" + }, + { + "path": "../packages/transport-webtransport" + } + ] +} diff --git a/package.json b/package.json index db12d779e4..11e16af991 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ ] }, "workspaces": [ + "interop", "packages/*" ] } diff --git a/packages/crypto/.aegir.js b/packages/crypto/.aegir.js new file mode 100644 index 0000000000..3cda4fd788 --- /dev/null +++ b/packages/crypto/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir/types').PartialOptions} */ +export default { + build: { + bundlesizeMax: '70kB' + } +} diff --git a/packages/crypto/CHANGELOG.md b/packages/crypto/CHANGELOG.md new file mode 100644 index 0000000000..2185a39d35 --- /dev/null +++ b/packages/crypto/CHANGELOG.md @@ -0,0 +1,738 @@ +## [1.0.17](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.16...v1.0.17) (2023-05-05) + + +### Dependencies + +* **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)) + +## [1.0.16](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.15...v1.0.16) (2023-05-05) + + +### Bug Fixes + +* Remove unreliable webkit-linux useragent check ([#319](https://github.com/libp2p/js-libp2p-crypto/issues/319)) ([8f8df5c](https://github.com/libp2p/js-libp2p-crypto/commit/8f8df5ccb99cf49f8d7f85a4834c505738c3bf63)), closes [#318](https://github.com/libp2p/js-libp2p-crypto/issues/318) + +## [1.0.15](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.14...v1.0.15) (2023-04-04) + + +### Bug Fixes + +* deriveKey in webkit linux (workaround) ([#313](https://github.com/libp2p/js-libp2p-crypto/issues/313)) ([4905944](https://github.com/libp2p/js-libp2p-crypto/commit/4905944fdb0578a77a36fd5c097be459a501ad34)) + +## [1.0.14](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.13...v1.0.14) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#297](https://github.com/libp2p/js-libp2p-crypto/issues/297)) ([7e61930](https://github.com/libp2p/js-libp2p-crypto/commit/7e61930f0416bac16dfd66f825a60a49acfbf780)) + +## [1.0.13](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.12...v1.0.13) (2023-03-10) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([b66007c](https://github.com/libp2p/js-libp2p-crypto/commit/b66007c8a092789490789dce596b4e157fa855e1)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([110063c](https://github.com/libp2p/js-libp2p-crypto/commit/110063cbea5d868923a2df2b9676e13a127b81f5)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([c789b0c](https://github.com/libp2p/js-libp2p-crypto/commit/c789b0c9f18f76df84a4038e9ed893a516babb43)) + + +### Dependencies + +* **dev:** upgrade aegir to `38.1.2` ([#302](https://github.com/libp2p/js-libp2p-crypto/issues/302)) ([9d60e39](https://github.com/libp2p/js-libp2p-crypto/commit/9d60e394d5e52167e9197067d3fa7953b30a990b)) + +## [1.0.12](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.11...v1.0.12) (2023-02-08) + + +### Bug Fixes + +* derive ed25519 public key from private key using node crypto ([#300](https://github.com/libp2p/js-libp2p-crypto/issues/300)) ([874f820](https://github.com/libp2p/js-libp2p-crypto/commit/874f8201c157a76a5fd9d3828c1f44358bce2f48)), closes [#295](https://github.com/libp2p/js-libp2p-crypto/issues/295) + + +### Trivial Changes + +* replace err-code with CodeError ([#293](https://github.com/libp2p/js-libp2p-crypto/issues/293)) ([4398cf6](https://github.com/libp2p/js-libp2p-crypto/commit/4398cf6c719fc7cb051cb3071cad9fc86b858752)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) + +## [1.0.11](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.10...v1.0.11) (2023-01-06) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#292](https://github.com/libp2p/js-libp2p-crypto/issues/292)) ([f2d78f8](https://github.com/libp2p/js-libp2p-crypto/commit/f2d78f8012b459da0a62bb4a7c63c396f56d4976)), closes [#234](https://github.com/libp2p/js-libp2p-crypto/issues/234) [#226](https://github.com/libp2p/js-libp2p-crypto/issues/226) [#234](https://github.com/libp2p/js-libp2p-crypto/issues/234) [#226](https://github.com/libp2p/js-libp2p-crypto/issues/226) [#226](https://github.com/libp2p/js-libp2p-crypto/issues/226) + + +### Trivial Changes + +* update bundlesize ([c2a1954](https://github.com/libp2p/js-libp2p-crypto/commit/c2a195431ba94fdeedfeb89e406b80a60f960dd7)) + +## [1.0.10](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.9...v1.0.10) (2022-12-16) + + +### Documentation + +* publish api docs ([#291](https://github.com/libp2p/js-libp2p-crypto/issues/291)) ([5ae970f](https://github.com/libp2p/js-libp2p-crypto/commit/5ae970f0190853470a24509c3e396cc949294d8e)) + +## [1.0.9](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.8...v1.0.9) (2022-12-01) + + +### Bug Fixes + +* use node crypto for ed25519 signing and verification ([#289](https://github.com/libp2p/js-libp2p-crypto/issues/289)) ([1c623e7](https://github.com/libp2p/js-libp2p-crypto/commit/1c623e7d55ddfafbad0b65b261f55bbc6957df28)) + +## [1.0.8](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.7...v1.0.8) (2022-12-01) + + +### Dependencies + +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#287](https://github.com/libp2p/js-libp2p-crypto/issues/287)) ([3aa22cd](https://github.com/libp2p/js-libp2p-crypto/commit/3aa22cdfdef028b3b451e62f9f5784000b2b0690)) + +## [1.0.7](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.6...v1.0.7) (2022-10-14) + + +### Dependencies + +* update protons-runtime and protons ([#283](https://github.com/libp2p/js-libp2p-crypto/issues/283)) ([2c4650c](https://github.com/libp2p/js-libp2p-crypto/commit/2c4650cff5222344b2573be6db4f791415dc276c)) + +## [1.0.6](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.5...v1.0.6) (2022-10-13) + + +### Dependencies + +* bump uint8arrays from 3.1.1 to 4.0.2 ([#281](https://github.com/libp2p/js-libp2p-crypto/issues/281)) ([2e87325](https://github.com/libp2p/js-libp2p-crypto/commit/2e8732549814bead1f0da1f2afc5b591f1dfc765)) + +## [1.0.5](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.4...v1.0.5) (2022-10-13) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([05c2246](https://github.com/libp2p/js-libp2p-crypto/commit/05c22463b3334ef9dd2c74a29a5f2a5fca96222a)) + + +### Dependencies + +* bump multiformats from 9.9.0 to 10.0.0 ([#279](https://github.com/libp2p/js-libp2p-crypto/issues/279)) ([699c812](https://github.com/libp2p/js-libp2p-crypto/commit/699c812e07e35cf81b199698ea362d507777fbd9)) +* **dev:** bump @types/mocha from 9.1.1 to 10.0.0 ([#276](https://github.com/libp2p/js-libp2p-crypto/issues/276)) ([bb22d31](https://github.com/libp2p/js-libp2p-crypto/commit/bb22d314c1c1ea6d1e4d4d0bfcbd76969e1f3ab1)) + +## [1.0.4](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.3...v1.0.4) (2022-08-26) + + +### Dependencies + +* **dev:** bump wherearewe from 1.0.2 to 2.0.1 ([#273](https://github.com/libp2p/js-libp2p-crypto/issues/273)) ([612e8ae](https://github.com/libp2p/js-libp2p-crypto/commit/612e8aef341d1f3392eed047fdc66d9c329cbf1f)) + +## [1.0.3](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.2...v1.0.3) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#272](https://github.com/libp2p/js-libp2p-crypto/issues/272)) ([23a031a](https://github.com/libp2p/js-libp2p-crypto/commit/23a031ab554677ee0b7c5d632e5457edf5615954)) + +## [1.0.2](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.1...v1.0.2) (2022-08-03) + + +### Bug Fixes + +* remove iso-random-stream dependency ([#262](https://github.com/libp2p/js-libp2p-crypto/issues/262)) ([c5cb096](https://github.com/libp2p/js-libp2p-crypto/commit/c5cb0969a614fb9cab73a5e12acc5e9e8ce9993d)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.0...v1.0.1) (2022-07-31) + + +### Trivial Changes + +* update project config ([#265](https://github.com/libp2p/js-libp2p-crypto/issues/265)) ([10ca181](https://github.com/libp2p/js-libp2p-crypto/commit/10ca18126bacf97e34283cc82c4bc8500716a4e3)) + + +### Dependencies + +* update protons to support no-copy ops ([#268](https://github.com/libp2p/js-libp2p-crypto/issues/268)) ([920b081](https://github.com/libp2p/js-libp2p-crypto/commit/920b081bb88afe094cbedd84a2113cf29ef9f1e8)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.14...v1.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#260](https://github.com/libp2p/js-libp2p-crypto/issues/260)) ([60e0789](https://github.com/libp2p/js-libp2p-crypto/commit/60e078968de7d03a61fde6cfd4ab5a3e378c127f)) + +### [0.22.14](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.13...v0.22.14) (2022-05-23) + + +### Bug Fixes + +* Added .js extension to import in src/keys/jwk2pem.ts ([#257](https://github.com/libp2p/js-libp2p-crypto/issues/257)) ([9e8f376](https://github.com/libp2p/js-libp2p-crypto/commit/9e8f3767f5f051edc09ae7be77c833817fed7279)), closes [#256](https://github.com/libp2p/js-libp2p-crypto/issues/256) + + +### Trivial Changes + +* **deps:** bump @libp2p/interfaces from 1.3.32 to 2.0.1 ([#258](https://github.com/libp2p/js-libp2p-crypto/issues/258)) ([f7d30bc](https://github.com/libp2p/js-libp2p-crypto/commit/f7d30bce64a74711e56b962412989164530a45d3)) + +### [0.22.13](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.12...v0.22.13) (2022-05-10) + + +### Trivial Changes + +* **deps-dev:** bump sinon from 13.0.2 to 14.0.0 ([#253](https://github.com/libp2p/js-libp2p-crypto/issues/253)) ([f999025](https://github.com/libp2p/js-libp2p-crypto/commit/f999025ef6f9a2a5ad86e60a6ff95cc4bcef9f06)) + +### [0.22.12](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.11...v0.22.12) (2022-05-10) + + +### Bug Fixes + +* encode protobuf enums correctly ([#254](https://github.com/libp2p/js-libp2p-crypto/issues/254)) ([a27dc64](https://github.com/libp2p/js-libp2p-crypto/commit/a27dc64a902d947d58fd87ed7c532a66f81eaedd)) + +### [0.22.11](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.10...v0.22.11) (2022-04-14) + + +### Bug Fixes + +* add return types to key methods ([#252](https://github.com/libp2p/js-libp2p-crypto/issues/252)) ([8363d28](https://github.com/libp2p/js-libp2p-crypto/commit/8363d28eda3a827ff03a6fc29df263b8c474cfb9)) + +### [0.22.10](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.9...v0.22.10) (2022-04-11) + + +### Bug Fixes + +* use protons ([#251](https://github.com/libp2p/js-libp2p-crypto/issues/251)) ([54a1424](https://github.com/libp2p/js-libp2p-crypto/commit/54a14249ca5e3b7395c243a762fbe6bb4c96e24f)) + +### [0.22.9](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.8...v0.22.9) (2022-03-10) + + +### Bug Fixes + +* fix broken return type ([#247](https://github.com/libp2p/js-libp2p-crypto/issues/247)) ([afa5b6d](https://github.com/libp2p/js-libp2p-crypto/commit/afa5b6d5a4a5e34e4fcac07803439f32555cb748)) + +### [0.22.8](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.7...v0.22.8) (2022-02-21) + + +### Bug Fixes + +* bump noble-ed25519 to an audited version ([#245](https://github.com/libp2p/js-libp2p-crypto/issues/245)) ([a104a4f](https://github.com/libp2p/js-libp2p-crypto/commit/a104a4f0a0fa5b2ecefd8928c3963dfa1019355b)) + +### [0.22.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.6...v0.22.7) (2022-02-03) + + +### Trivial Changes + +* **deps-dev:** bump sinon from 12.0.1 to 13.0.1 ([#243](https://github.com/libp2p/js-libp2p-crypto/issues/243)) ([a484751](https://github.com/libp2p/js-libp2p-crypto/commit/a484751847ca6ed1889602b378a1436a84483973)) + +### [0.22.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.5...v0.22.6) (2022-01-17) + + +### Trivial Changes + +* update benchmark deps ([#238](https://github.com/libp2p/js-libp2p-crypto/issues/238)) ([21ffe04](https://github.com/libp2p/js-libp2p-crypto/commit/21ffe0428fa040088f1b91f4e454418f436ed690)) + +### [0.22.5](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.4...v0.22.5) (2022-01-15) + + +### Trivial Changes + +* update project config ([#237](https://github.com/libp2p/js-libp2p-crypto/issues/237)) ([0f28984](https://github.com/libp2p/js-libp2p-crypto/commit/0f28984dee75d446d03911e271d242f1dbc2e8f9)) + +### [0.22.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.3...v0.22.4) (2022-01-08) + + +### Trivial Changes + +* change min node support to 16 ([#230](https://github.com/libp2p/js-libp2p-crypto/issues/230)) ([9aabfe6](https://github.com/libp2p/js-libp2p-crypto/commit/9aabfe6c885b132366c2772e834f8396c96081e9)), closes [#221](https://github.com/libp2p/js-libp2p-crypto/issues/221) +* **deps:** bump node-forge from 0.10.0 to 1.1.0 ([#231](https://github.com/libp2p/js-libp2p-crypto/issues/231)) ([d33eea1](https://github.com/libp2p/js-libp2p-crypto/commit/d33eea1137c538668f1a54b3bc493e5fb4d1c035)) + +### [0.22.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.2...v0.22.3) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#232](https://github.com/libp2p/js-libp2p-crypto/issues/232)) ([d1b9961](https://github.com/libp2p/js-libp2p-crypto/commit/d1b99617da775ffed7a7e1a20de740338c0c6159)) +* uncomment renderer build ([#226](https://github.com/libp2p/js-libp2p-crypto/issues/226)) ([cecad66](https://github.com/libp2p/js-libp2p-crypto/commit/cecad669aa7dda5e5eed178099ae0191a500e543)) +* update build ([#227](https://github.com/libp2p/js-libp2p-crypto/issues/227)) ([1642c1b](https://github.com/libp2p/js-libp2p-crypto/commit/1642c1b4386a6d28ed945ffb96ce68ef8a2aed35)) +* update build ([#229](https://github.com/libp2p/js-libp2p-crypto/issues/229)) ([3544d9c](https://github.com/libp2p/js-libp2p-crypto/commit/3544d9c63a7fba0b39e7cbf23bc44b4e196ad52b)) + +# [0.22.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.21.0...v0.22.0) (2022-01-04) + + +### Features + +* convert to typescript ([#222](https://github.com/libp2p/js-libp2p-crypto/issues/222)) ([7875906](https://github.com/libp2p/js-libp2p-crypto/commit/7875906c71fa9fe695889d57061937dd6494b15a)) + + + +# [0.21.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.20.0...v0.21.0) (2021-12-01) + + +### Features + +* replace keypair and ursa-optional with node crypto ([#219](https://github.com/libp2p/js-libp2p-crypto/issues/219)) ([759535c](https://github.com/libp2p/js-libp2p-crypto/commit/759535cef65f0141c540524b77c72783288cc0a0)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +# [0.20.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.7...v0.20.0) (2021-10-21) + + +### Bug Fixes + +* demand up to date keypair ([#201](https://github.com/libp2p/js-libp2p-crypto/issues/201)) ([2e40aea](https://github.com/libp2p/js-libp2p-crypto/commit/2e40aeaaa6735a4f2bea58f568d937b60561868b)) + + +### Features + +* use noble-secp256k1 and noble-ed25519 ([#202](https://github.com/libp2p/js-libp2p-crypto/issues/202)) ([167eace](https://github.com/libp2p/js-libp2p-crypto/commit/167eaceb61a779904ff006602ce58d7065d126b7)) + + +### BREAKING CHANGES + +* keys function hashAndVerify returns boolean false when fail, instead of throwing error + + + +## [0.19.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.6...v0.19.7) (2021-08-18) + + + +## [0.19.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.5...v0.19.6) (2021-07-15) + + + +## [0.19.5](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.4...v0.19.5) (2021-07-07) + + + +## [0.19.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.3...v0.19.4) (2021-04-20) + + +### Bug Fixes + +* specify pbjs root ([#188](https://github.com/libp2p/js-libp2p-crypto/issues/188)) ([a3da59b](https://github.com/libp2p/js-libp2p-crypto/commit/a3da59b90e50d514d1578236e1c86f05c8cb0805)) + + + +## [0.19.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.7...v0.19.3) (2021-04-11) + + +### Bug Fixes + +* ed25519 key ID generation ([bc33769](https://github.com/libp2p/js-libp2p-crypto/commit/bc337698b6124e3461c8dc4be2f264ea98351c70)) +* ed25519 PeerID generation ([#186](https://github.com/libp2p/js-libp2p-crypto/issues/186)) ([1c16dd3](https://github.com/libp2p/js-libp2p-crypto/commit/1c16dd3dec8a641f55187bd9fbb6c03ba5fafdaa)), closes [ipfs/js-ipfs#3591](https://github.com/ipfs/js-ipfs/issues/3591) [libp2p/js-libp2p-crypto#185](https://github.com/libp2p/js-libp2p-crypto/issues/185) +* go ed25519 interop ([2f18a07](https://github.com/libp2p/js-libp2p-crypto/commit/2f18a077b47ee84c450431f7431ecdfc913c8543)) +* remove rendundant public key ([#181](https://github.com/libp2p/js-libp2p-crypto/issues/181)) ([afcffc8](https://github.com/libp2p/js-libp2p-crypto/commit/afcffc8115c8833edfe2a942d05547f418be5585)) +* replace node buffers with uint8arrays ([#180](https://github.com/libp2p/js-libp2p-crypto/issues/180)) ([a0f387a](https://github.com/libp2p/js-libp2p-crypto/commit/a0f387aeab5dff45368341d0d80a5d1a25e9f849)) + + +### Features + +* add exporting/importing of non rsa keys in libp2p-key format ([#179](https://github.com/libp2p/js-libp2p-crypto/issues/179)) ([7273739](https://github.com/libp2p/js-libp2p-crypto/commit/7273739f045b33a46aae45f5003dd09f7ea6e37e)) + + +### BREAKING CHANGES + +* The private ed25519 key will no longer include the redundant public key + +* chore: fix lint + + + + +## [0.19.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.1...v0.19.2) (2021-03-17) + + +### Bug Fixes + +* ed25519 PeerID generation ([#186](https://github.com/libp2p/js-libp2p-crypto/issues/186)) ([1c16dd3](https://github.com/libp2p/js-libp2p-crypto/commit/1c16dd3)), closes [ipfs/js-ipfs#3591](https://github.com/ipfs/js-ipfs/issues/3591) [libp2p/js-libp2p-crypto#185](https://github.com/libp2p/js-libp2p-crypto/issues/185) + + + + +## [0.19.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.0...v0.19.1) (2021-03-15) + + +### Bug Fixes + +* ed25519 key ID generation ([bc33769](https://github.com/libp2p/js-libp2p-crypto/commit/bc33769)) + + + + +# [0.19.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.18.0...v0.19.0) (2021-01-15) + + + + +# [0.18.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.9...v0.18.0) (2020-08-07) + + +### Bug Fixes + +* remove rendundant public key ([#181](https://github.com/libp2p/js-libp2p-crypto/issues/181)) ([afcffc8](https://github.com/libp2p/js-libp2p-crypto/commit/afcffc8)) +* replace node buffers with uint8arrays ([#180](https://github.com/libp2p/js-libp2p-crypto/issues/180)) ([a0f387a](https://github.com/libp2p/js-libp2p-crypto/commit/a0f387a)) + + +### BREAKING CHANGES + +* The private ed25519 key will no longer include the redundant public key + +* chore: fix lint +* - Where node Buffers were returned, now Uint8Arrays are + +* chore: remove commented code + + + + +## [0.17.9](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.8...v0.17.9) (2020-08-05) + + +### Features + +* add exporting/importing of non rsa keys in libp2p-key format ([#179](https://github.com/libp2p/js-libp2p-crypto/issues/179)) ([7273739](https://github.com/libp2p/js-libp2p-crypto/commit/7273739)) + + + + +## [0.17.8](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.7...v0.17.8) (2020-07-20) + + +### Bug Fixes + +* go ed25519 interop ([2f18a07](https://github.com/libp2p/js-libp2p-crypto/commit/2f18a07)) + + + + +## [0.17.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.6...v0.17.7) (2020-06-09) + + + + +## [0.17.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.5...v0.17.6) (2020-04-07) + + +### Bug Fixes + +* add buffer and update deps ([#25](https://github.com/libp2p/js-libp2p-crypto/issues/25)) ([35f196e](https://github.com/libp2p/js-libp2p-crypto/commit/35f196e)) +* **unmarshal:** provide only one arg to callback ([#17](https://github.com/libp2p/js-libp2p-crypto/issues/17)) ([3bb8451](https://github.com/libp2p/js-libp2p-crypto/commit/3bb8451)) +* circular circular dep -> DI ([0dcf1a6](https://github.com/libp2p/js-libp2p-crypto/commit/0dcf1a6)) +* update deps and repo setup ([cfdcbe0](https://github.com/libp2p/js-libp2p-crypto/commit/cfdcbe0)) + + +### Features + +* add `id()` method to Secp256k1PrivateKey ([f4dbd62](https://github.com/libp2p/js-libp2p-crypto/commit/f4dbd62)) +* initial implementation ([4c36aeb](https://github.com/libp2p/js-libp2p-crypto/commit/4c36aeb)) +* next libp2p-crypto ([#4](https://github.com/libp2p/js-libp2p-crypto/issues/4)) ([4ee48a7](https://github.com/libp2p/js-libp2p-crypto/commit/4ee48a7)) +* use async await ([#18](https://github.com/libp2p/js-libp2p-crypto/issues/18)) ([1974eb9](https://github.com/libp2p/js-libp2p-crypto/commit/1974eb9)) + + +### BREAKING CHANGES + +* Callback support has been dropped in favor of async/await. + +* feat: use async/await + +This PR changes this module to remove callbacks and use async/await. The API is unchanged aside from the obvious removal of the `callback` parameter. + +refs https://github.com/ipfs/js-ipfs/issues/1670 + +* fix: use latest multihashing-async as it is all promises now + + + + +## [0.17.5](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.4...v0.17.5) (2020-03-24) + + + + +## [0.17.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.3...v0.17.4) (2020-03-23) + + +### Bug Fixes + +* add buffer, cleanup, reduce size ([#170](https://github.com/libp2p/js-libp2p-crypto/issues/170)) ([c956d1a](https://github.com/libp2p/js-libp2p-crypto/commit/c956d1a)) + + + + +## [0.17.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.2...v0.17.3) (2020-02-26) + + +### Performance Improvements + +* remove asn1.js and use node-forge ([#166](https://github.com/libp2p/js-libp2p-crypto/issues/166)) ([00477e3](https://github.com/libp2p/js-libp2p-crypto/commit/00477e3)) +* remove jwk2privPem and jwk2pubPem ([#162](https://github.com/libp2p/js-libp2p-crypto/issues/162)) ([cc20949](https://github.com/libp2p/js-libp2p-crypto/commit/cc20949)) + + +### BREAKING CHANGES + +* removes unused jwk2pem methods `jwk2pubPem` and `jwk2privPem`. These methods are not being used in any js libp2p modules, so only users referencing these directly will be impacted. + + + + +## [0.17.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.1...v0.17.2) (2020-01-17) + + +### Features + +* add typescript types + linting/tests ([#161](https://github.com/libp2p/js-libp2p-crypto/issues/161)) ([e01977c](https://github.com/libp2p/js-libp2p-crypto/commit/e01977c)) + + + + +## [0.17.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.0...v0.17.1) (2019-10-25) + + +### Bug Fixes + +* better error for missing web crypto ([a5e0560](https://github.com/libp2p/js-libp2p-crypto/commit/a5e0560)) +* browser rsa enc/dec ([b8e2414](https://github.com/libp2p/js-libp2p-crypto/commit/b8e2414)) +* jwk var naming ([8b8d0c1](https://github.com/libp2p/js-libp2p-crypto/commit/8b8d0c1)) +* lint ([2c294b5](https://github.com/libp2p/js-libp2p-crypto/commit/2c294b5)) +* padding error ([2c1bac5](https://github.com/libp2p/js-libp2p-crypto/commit/2c1bac5)) +* use direct buffers instead of converting to hex ([027a5a9](https://github.com/libp2p/js-libp2p-crypto/commit/027a5a9)) + + +### Features + +* add (rsa)pubKey.encrypt and (rsa)privKey.decrypt ([34c5f5c](https://github.com/libp2p/js-libp2p-crypto/commit/34c5f5c)) +* browser enc/dec ([9f747a1](https://github.com/libp2p/js-libp2p-crypto/commit/9f747a1)) +* use forge to convert jwk2forge ([b998f63](https://github.com/libp2p/js-libp2p-crypto/commit/b998f63)) + + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.16.1...v0.17.0) (2019-07-11) + + +### Bug Fixes + +* **deps:** update to ursa-optiona@0.10 ([26b6217](https://github.com/libp2p/js-libp2p-crypto/commit/26b6217)) +* fix links in README ([#148](https://github.com/libp2p/js-libp2p-crypto/issues/148)) ([5cd0e8c](https://github.com/libp2p/js-libp2p-crypto/commit/5cd0e8c)) +* put optional args last for key export ([#154](https://github.com/libp2p/js-libp2p-crypto/issues/154)) ([d675670](https://github.com/libp2p/js-libp2p-crypto/commit/d675670)) + + +### Features + +* refactor to use async/await ([#131](https://github.com/libp2p/js-libp2p-crypto/issues/131)) ([ad71072](https://github.com/libp2p/js-libp2p-crypto/commit/ad71072)) + + +### BREAKING CHANGES + +* key export arguments are now swapped so that the optional format is last +* API refactored to use async/await + +feat: WIP use async await +fix: passing tests +chore: update travis node.js versions +fix: skip ursa optional tests on windows +fix: benchmarks +docs: update docs +fix: remove broken and intested private key decrypt +chore: update deps + + + + +## [0.16.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.16.0...v0.16.1) (2019-02-26) + + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.15.0...v0.16.0) (2019-01-08) + + +### Bug Fixes + +* clean up, bundle size reduction ([8d8294d](https://github.com/libp2p/js-libp2p-crypto/commit/8d8294d)) + + +### BREAKING CHANGES + +* getRandomValues method exported from src/keys/rsa-browser.js and src/keys/rsa.js signature has changed from accepting an array to a number for random byte length + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.14.1...v0.15.0) (2019-01-03) + + +### Features + +* nextTick instead of setImmediate, and fix sync in async ([#136](https://github.com/libp2p/js-libp2p-crypto/issues/136)) ([c54ea20](https://github.com/libp2p/js-libp2p-crypto/commit/c54ea20)) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.14.0...v0.14.1) (2018-11-05) + + +### Bug Fixes + +* dont setimmediate when its not needed ([9e57786](https://github.com/libp2p/js-libp2p-crypto/commit/9e57786)) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.13.0...v0.14.0) (2018-09-17) + + +### Bug Fixes + +* windows build ([c7e0409](https://github.com/libp2p/js-libp2p-crypto/commit/c7e0409)) +* **lint:** use ~ for ursa-optional version ([e8cbf13](https://github.com/libp2p/js-libp2p-crypto/commit/e8cbf13)) + + +### Features + +* use ursa-optional for lightning fast key generation ([b05e77f](https://github.com/libp2p/js-libp2p-crypto/commit/b05e77f)) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.12.1...v0.13.0) (2018-04-05) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.12.0...v0.12.1) (2018-02-12) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.11.0...v0.12.0) (2018-01-27) + + +### Features + +* improve perf ([#117](https://github.com/libp2p/js-libp2p-crypto/issues/117)) ([cdcca5f](https://github.com/libp2p/js-libp2p-crypto/commit/cdcca5f)) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.4...v0.11.0) (2017-12-20) + + +### Features + +* key exchange with jsrsasign ([#115](https://github.com/libp2p/js-libp2p-crypto/issues/115)) ([b342128](https://github.com/libp2p/js-libp2p-crypto/commit/b342128)) + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.3...v0.10.4) (2017-12-01) + + +### Bug Fixes + +* catch error when unmarshaling instead of crashing ([#113](https://github.com/libp2p/js-libp2p-crypto/issues/113)) ([7608fdd](https://github.com/libp2p/js-libp2p-crypto/commit/7608fdd)) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.2...v0.10.3) (2017-09-07) + + +### Features + +* switch protocol-buffers to protons ([#110](https://github.com/libp2p/js-libp2p-crypto/issues/110)) ([3a91ae2](https://github.com/libp2p/js-libp2p-crypto/commit/3a91ae2)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.1...v0.10.2) (2017-09-06) + + +### Bug Fixes + +* use regular protocol-buffers until protobufjs is fixed ([#109](https://github.com/libp2p/js-libp2p-crypto/issues/109)) ([957fdd3](https://github.com/libp2p/js-libp2p-crypto/commit/957fdd3)) + + +### Features + +* **deps:** upgrade to aegir@12 and browserify-aes@1.0.8 ([83257bc](https://github.com/libp2p/js-libp2p-crypto/commit/83257bc)) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.0...v0.10.1) (2017-09-05) + + +### Bug Fixes + +* switch to protobufjs ([#107](https://github.com/libp2p/js-libp2p-crypto/issues/107)) ([dc2793f](https://github.com/libp2p/js-libp2p-crypto/commit/dc2793f)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.4...v0.10.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#106](https://github.com/libp2p/js-libp2p-crypto/issues/106)) ([9e977c7](https://github.com/libp2p/js-libp2p-crypto/commit/9e977c7)) +* skip nextTick in nodeify ([#103](https://github.com/libp2p/js-libp2p-crypto/issues/103)) ([f20267b](https://github.com/libp2p/js-libp2p-crypto/commit/f20267b)) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.3...v0.9.4) (2017-07-22) + + +### Bug Fixes + +* circular circular dep -> DI ([bc554d1](https://github.com/libp2p/js-libp2p-crypto/commit/bc554d1)) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.2...v0.9.3) (2017-07-22) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.1...v0.9.2) (2017-07-22) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.0...v0.9.1) (2017-07-22) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.8...v0.9.0) (2017-07-22) + + + + +## [0.8.8](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.7...v0.8.8) (2017-04-11) + + +### Bug Fixes + +* **ecdh:** allow base64 to be left-0-padded, needed for JWK format ([be64372](https://github.com/libp2p/js-libp2p-crypto/commit/be64372)), closes [#97](https://github.com/libp2p/js-libp2p-crypto/issues/97) + + + + +## [0.8.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.6...v0.8.7) (2017-03-21) + + + + +## [0.8.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.5...v0.8.6) (2017-03-03) + + +### Bug Fixes + +* **package:** update tweetnacl to version 1.0.0-rc.1 ([4e56e17](https://github.com/libp2p/js-libp2p-crypto/commit/4e56e17)) + + +### Features + +* **keys:** implement generateKeyPairFromSeed for ed25519 ([e5b7c1f](https://github.com/libp2p/js-libp2p-crypto/commit/e5b7c1f)) diff --git a/packages/crypto/LICENSE b/packages/crypto/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/crypto/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/crypto/LICENSE-APACHE b/packages/crypto/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/crypto/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/crypto/LICENSE-MIT b/packages/crypto/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/crypto/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/crypto/README.md b/packages/crypto/README.md new file mode 100644 index 0000000000..a1ab936c74 --- /dev/null +++ b/packages/crypto/README.md @@ -0,0 +1,330 @@ +# @libp2p/crypto + +[![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) + +> Crypto primitives for libp2p + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +This repo contains the JavaScript implementation of the crypto primitives needed for libp2p. This is based on this [go implementation](https://github.com/libp2p/go-libp2p-crypto). + +## Lead Maintainer + +[Jacob Heun](https://github.com/jacobheun/) + +## Usage + +```js +const crypto = require('libp2p-crypto') + +// Now available to you: +// +// crypto.aes +// crypto.hmac +// crypto.keys +// etc. +// +// See full API details below... +``` + +### Web Crypto API + +The `libp2p-crypto` library depends on the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) in the browser. Web Crypto is available in all modern browsers, however browsers restrict its usage to [Secure Contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). + +**This means you will not be able to use some `libp2p-crypto` functions in the browser when the page is served over HTTP.** To enable the Web Crypto API and allow `libp2p-crypto` to work fully, please serve your page over HTTPS. + +## API + +### `crypto.aes` + +Exposes an interface to AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197. + +This uses `CTR` mode. + +#### `crypto.aes.create(key, iv)` + +- `key: Uint8Array` The key, if length `16` then `AES 128` is used. For length `32`, `AES 256` is used. +- `iv: Uint8Array` Must have length `16`. + +Returns `Promise<{decrypt, encrypt}>` + +##### `decrypt(data)` + +- `data: Uint8Array` + +Returns `Promise` + +##### `encrypt(data)` + +- `data: Uint8Array` + +Returns `Promise` + +```js +const crypto = require('libp2p-crypto') + +// Setting up Key and IV + +// A 16 bytes array, 128 Bits, AES-128 is chosen +const key128 = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) + +// A 16 bytes array, 128 Bits, +const IV = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) + +async function main () { + const decryptedMessage = 'Hello, world!' + + // Encrypting + const cipher = await crypto.aes.create(key128, IV) + const encryptedBuffer = await cipher.encrypt(Uint8Array.from(decryptedMessage)) + console.log(encryptedBuffer) + // prints: + + // Decrypting + const decipher = await crypto.aes.create(key128, IV) + const decryptedBuffer = await cipher.decrypt(encryptedBuffer) + + console.log(decryptedBuffer) + // prints: + + console.log(decryptedBuffer.toString('utf-8')) + // prints: Hello, world! +} + +main() +``` + +### `crypto.hmac` + +Exposes an interface to the Keyed-Hash Message Authentication Code (HMAC) as defined in U.S. Federal Information Processing Standards Publication 198. An HMAC is a cryptographic hash that uses a key to sign a message. The receiver verifies the hash by recomputing it using the same key. + +#### `crypto.hmac.create(hash, secret)` + +- `hash: String` +- `secret: Uint8Array` + +Returns `Promise<{digest}>` + +##### `digest(data)` + +- `data: Uint8Array` + +Returns `Promise` + +Example: + +```js +const crypto = require('libp2p-crypto') + +async function main () { + const hash = 'SHA1' // 'SHA256' || 'SHA512' + const hmac = await crypto.hmac.create(hash, uint8ArrayFromString('secret')) + const sig = await hmac.digest(uint8ArrayFromString('hello world')) + console.log(sig) +} + +main() +``` + +### `crypto.keys` + +**Supported Key Types** + +The [`generateKeyPair`](#generatekeypairtype-bits), [`marshalPublicKey`](#marshalpublickeykey-type), and [`marshalPrivateKey`](#marshalprivatekeykey-type) functions accept a string `type` argument. + +Currently the `'RSA'`, `'ed25519'`, and `secp256k1` types are supported, although ed25519 and secp256k1 keys support only signing and verification of messages. For encryption / decryption support, RSA keys should be used. + +### `crypto.keys.generateKeyPair(type, bits)` + +- `type: String`, see [Supported Key Types](#supported-key-types) above. +- `bits: Number` Minimum of 1024 + +Returns `Promise<{privateKey, publicKey}>` + +Generates a keypair of the given type and bitsize. + +### `crypto.keys.generateEphemeralKeyPair(curve)` + +- `curve: String`, one of `'P-256'`, `'P-384'`, `'P-521'` is currently supported + +Returns `Promise` + +Generates an ephemeral public key and returns a function that will compute the shared secret key. + +Focuses only on ECDH now, but can be made more general in the future. + +Resolves to an object of the form: + +```js +{ + key: Uint8Array, + genSharedKey: Function +} +``` + +### `crypto.keys.keyStretcher(cipherType, hashType, secret)` + +- `cipherType: String`, one of `'AES-128'`, `'AES-256'`, `'Blowfish'` +- `hashType: String`, one of `'SHA1'`, `SHA256`, `SHA512` +- `secret: Uint8Array` + +Returns `Promise` + +Generates a set of keys for each party by stretching the shared key. + +Resolves to an object of the form: + +```js +{ + k1: { + iv: Uint8Array, + cipherKey: Uint8Array, + macKey: Uint8Array + }, + k2: { + iv: Uint8Array, + cipherKey: Uint8Array, + macKey: Uint8Array + } +} +``` + +### `crypto.keys.marshalPublicKey(key, [type])` + +- `key: keys.rsa.RsaPublicKey | keys.ed25519.Ed25519PublicKey | keys.secp256k1.Secp256k1PublicKey` +- `type: String`, see [Supported Key Types](#supported-key-types) above. Defaults to 'rsa'. + +Returns `Uint8Array` + +Converts a public key object into a protobuf serialized public key. + +### `crypto.keys.unmarshalPublicKey(buf)` + +- `buf: Uint8Array` + +Returns `RsaPublicKey|Ed25519PublicKey|Secp256k1PublicKey` + +Converts a protobuf serialized public key into its representative object. + +### `crypto.keys.marshalPrivateKey(key, [type])` + +- `key: keys.rsa.RsaPrivateKey | keys.ed25519.Ed25519PrivateKey | keys.secp256k1.Secp256k1PrivateKey` +- `type: String`, see [Supported Key Types](#supported-key-types) above. + +Returns `Uint8Array` + +Converts a private key object into a protobuf serialized private key. + +### `crypto.keys.unmarshalPrivateKey(buf)` + +- `buf: Uint8Array` + +Returns `Promise` + +Converts a protobuf serialized private key into its representative object. + +### `crypto.keys.import(encryptedKey, password)` + +- `encryptedKey: string` +- `password: string` + +Returns `Promise` + +Converts an exported private key into its representative object. Supported formats are 'pem' (RSA only) and 'libp2p-key'. + +### `privateKey.export(password, format)` + +- `password: string` +- `format: string` the format to export to: 'pem' (rsa only), 'libp2p-key' + +Returns `string` + +Exports the password protected `PrivateKey`. RSA keys will be exported as password protected PEM by default. Ed25519 and Secp256k1 keys will be exported as password protected AES-GCM base64 encoded strings ('libp2p-key' format). + +### `crypto.randomBytes(number)` + +- `number: Number` + +Returns `Uint8Array` + +Generates a Uint8Array with length `number` populated by random bytes. + +### `crypto.pbkdf2(password, salt, iterations, keySize, hash)` + +- `password: String` +- `salt: String` +- `iterations: Number` +- `keySize: Number` in bytes +- `hash: String` the hashing algorithm ('sha1', 'sha2-512', ...) + +Computes the Password Based Key Derivation Function 2; returning a new password. + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/crypto/benchmark/ed25519/compat.cjs b/packages/crypto/benchmark/ed25519/compat.cjs new file mode 100644 index 0000000000..e8e31e4a47 --- /dev/null +++ b/packages/crypto/benchmark/ed25519/compat.cjs @@ -0,0 +1,201 @@ +/* eslint-disable no-console */ +// @ts-expect-error types are missing +const forge = require('node-forge/lib/forge') + +/* + * Make sure that every Ed25519 implementation can use keys generated + * by every other implementation to sign and verify messages signed + * by themselves and by every other implementation using those keys. + * + * Nb. some modules return different structures from their key generation + * routine - we normalise to `{ privateKey: seed, publicKey }`. + * + * Most implementations return the seed as the private key but supercop.wasm + * returns a hash of the seed. We ignore supercop's private key in favour + * of the seed here, since we can re-create it using the createKeyPair + * function because key generation is deterministic for a given seed. + */ + +const { concat } = require('uint8arrays/concat') +const { fromString } = require('uint8arrays/from-string') + +const native = require('ed25519') +const noble = require('@noble/ed25519') +const { randomBytes } = noble.utils +const { subtle } = require('crypto').webcrypto +require('node-forge/lib/ed25519') +const stable = require('@stablelib/ed25519') +const supercopWasm = require('supercop.wasm') + +const ALGORITHM = 'NODE-ED25519' +const ED25519_PKCS8_PREFIX = fromString('302e020100300506032b657004220420', 'hex') + +const implementations = [{ + name: '@noble/ed25519', + before: () => {}, + generateKeyPair: async () => { + const privateKey = noble.utils.randomPrivateKey() + const publicKey = await noble.getPublicKey(privateKey) + + return { + privateKey, + publicKey + } + }, + sign: (message, keyPair) => noble.sign(message, keyPair.privateKey), + verify: (message, signature, keyPair) => noble.verify(signature, message, keyPair.publicKey) +}, { + name: '@stablelib/ed25519', + before: () => {}, + generateKeyPair: async () => { + const key = stable.generateKeyPair() + + return { + privateKey: key.secretKey.subarray(0, 32), + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => stable.sign(concat([keyPair.privateKey, keyPair.publicKey]), message), + verify: (message, signature, keyPair) => stable.verify(keyPair.publicKey, message, signature) +}, { + name: 'node-forge/ed25519', + before: () => {}, + generateKeyPair: async () => { + const seed = randomBytes(32) + const key = await forge.pki.ed25519.generateKeyPair({ seed }) + + return { + privateKey: key.privateKey.subarray(0, 32), + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => forge.pki.ed25519.sign({ message, privateKey: keyPair.privateKey }), + verify: (message, signature, keyPair) => forge.pki.ed25519.verify({ signature, message, publicKey: keyPair.publicKey }) +}, { + name: 'supercop.wasm', + before: () => { + return new Promise(resolve => { + supercopWasm.ready(() => { + resolve() + }) + }) + }, + generateKeyPair: async () => { + const seed = supercopWasm.createSeed() + const key = supercopWasm.createKeyPair(seed) + + return { + privateKey: seed, + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => { + const key = supercopWasm.createKeyPair(keyPair.privateKey) + + return supercopWasm.sign(message, key.publicKey, key.secretKey) + }, + verify: (message, signature, keyPair) => { + return supercopWasm.verify(signature, message, keyPair.publicKey) + } +}, { + name: 'native Ed25519', + generateKeyPair: async () => { + const seed = randomBytes(32) + const key = native.MakeKeypair(seed) + + return { + privateKey: key.privateKey.subarray(0, 32), + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => native.Sign(message, keyPair.privateKey), + verify: (message, signature, keyPair) => native.Verify(message, signature, keyPair.publicKey) +}, { + name: 'node.js web crypto', + generateKeyPair: async () => { + const key = await subtle.generateKey({ + name: 'NODE-ED25519', + namedCurve: 'NODE-ED25519' + }, true, ['sign', 'verify']) + const jwk = await subtle.exportKey('jwk', key.privateKey) + + return { + privateKey: fromString(jwk.d, 'base64url'), + publicKey: fromString(jwk.x, 'base64url') + } + }, + sign: async (message, keyPair) => { + const pkcs8 = concat([ + ED25519_PKCS8_PREFIX, + keyPair.privateKey + ], ED25519_PKCS8_PREFIX.length + 32) + const cryptoKey = await subtle.importKey('pkcs8', pkcs8, { + name: ALGORITHM, + namedCurve: ALGORITHM + }, true, ['sign']) + + const signature = await subtle.sign(ALGORITHM, cryptoKey, message) + + return new Uint8Array(signature) + }, + verify: async (message, signature, keyPair) => { + const cryptoKey = await subtle.importKey('raw', keyPair.publicKey, { + name: ALGORITHM, + namedCurve: ALGORITHM, + public: true + }, true, ['verify']) + + return subtle.verify(ALGORITHM, cryptoKey, signature, message) + } +}] + +async function test (a, b) { + console.info(`test ${a.name} against ${b.name}`) + const message = Buffer.from('hello world ' + Math.random()) + + const keyPair = await a.generateKeyPair() + + if (keyPair.privateKey.length !== 32) { + throw new Error('Private key not 32 bytes') + } + + if (keyPair.publicKey.length !== 32) { + throw new Error('Public key not 32 bytes') + } + + // make sure we can sign and verify with keys created by the other implementation + const pairs = [[a, a], [a, b], [b, a], [b, b]] + + for (const [a, b] of pairs) { + console.info('test', a.name, 'against', b.name) + const signature = await a.sign(message, keyPair) + const isSigned = await b.verify(message, signature, keyPair) + + if (!isSigned) { + console.error(`${b.name} could not verify signature created by ${a.name}`) + } + } +} + +async function main () { + for (const a of implementations) { + if (a.before) { + await a.before() + } + + for (const b of implementations) { + if (b.before) { + await b.before() + } + + await test(a, b) + await test(b, a) + } + } +} + +main() + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/packages/crypto/benchmark/ed25519/index.js b/packages/crypto/benchmark/ed25519/index.js new file mode 100644 index 0000000000..c0f554e8a8 --- /dev/null +++ b/packages/crypto/benchmark/ed25519/index.js @@ -0,0 +1,138 @@ +/* eslint-disable no-console */ +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import Benchmark from 'benchmark' +import native from 'ed25519' +import * as noble from '@noble/ed25519' +import 'node-forge/lib/ed25519.js' +import stable from '@stablelib/ed25519' +import supercopWasm from 'supercop.wasm' +import ed25519WasmPro from 'ed25519-wasm-pro' +import * as libp2pCrypto from '../../dist/src/index.js' + +const { randomBytes } = noble.utils + +const suite = new Benchmark.Suite('ed25519 implementations') + +suite.add('@libp2p/crypto', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + + const key = await libp2pCrypto.keys.generateKeyPair('Ed25519') + + const signature = await key.sign(message) + const res = await key.public.verify(message, signature) + + if (!res) { + throw new Error('could not verify @libp2p/crypto signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('@noble/ed25519', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const privateKey = noble.utils.randomPrivateKey() + const publicKey = await noble.getPublicKey(privateKey) + const signature = await noble.sign(message, privateKey) + const isSigned = await noble.verify(signature, message, publicKey) + + if (!isSigned) { + throw new Error('could not verify noble signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('@stablelib/ed25519', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const key = stable.generateKeyPair() + const signature = await stable.sign(key.secretKey, message) + const isSigned = await stable.verify(key.publicKey, message, signature) + + if (!isSigned) { + throw new Error('could not verify stablelib signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('node-forge/ed25519', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = randomBytes(32) + const key = await forge.pki.ed25519.generateKeyPair({ seed }) + const signature = await forge.pki.ed25519.sign({ message, privateKey: key.privateKey }) + const res = await forge.pki.ed25519.verify({ signature, message, publicKey: key.publicKey }) + + if (!res) { + throw new Error('could not verify node-forge signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('supercop.wasm', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = supercopWasm.createSeed() + const keys = supercopWasm.createKeyPair(seed) + const signature = supercopWasm.sign(message, keys.publicKey, keys.secretKey) + const isSigned = await supercopWasm.verify(signature, message, keys.publicKey) + + if (!isSigned) { + throw new Error('could not verify supercop.wasm signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('ed25519-wasm-pro', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = ed25519WasmPro.createSeed() + const keys = ed25519WasmPro.createKeyPair(seed) + const signature = ed25519WasmPro.sign(message, keys.publicKey, keys.secretKey) + const isSigned = await ed25519WasmPro.verify(signature, message, keys.publicKey) + + if (!isSigned) { + throw new Error('could not verify ed25519-wasm-pro signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('ed25519 (native module)', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = randomBytes(32) + const key = native.MakeKeypair(seed) + const signature = native.Sign(message, key) + const res = native.Verify(message, signature, key.publicKey) + + if (!res) { + throw new Error('could not verify native signature') + } + + d.resolve() +}, { defer: true }) + +async function main () { + await Promise.all([ + new Promise((resolve) => { + supercopWasm.ready(() => resolve()) + }), + new Promise((resolve) => { + ed25519WasmPro.ready(() => resolve()) + }) + ]) + noble.utils.precompute(8) + + suite + .on('cycle', (event) => console.log(String(event.target))) + .on('complete', function () { + console.log('fastest is ' + this.filter('fastest').map('name')) + }) + .run({ async: true }) +} + +main() + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/packages/crypto/benchmark/ed25519/package.json b/packages/crypto/benchmark/ed25519/package.json new file mode 100644 index 0000000000..530a3d9c44 --- /dev/null +++ b/packages/crypto/benchmark/ed25519/package.json @@ -0,0 +1,20 @@ +{ + "name": "libp2p-crypto-ed25519-benchmarks", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node .", + "compat": "node compat.js" + }, + "license": "MIT", + "dependencies": { + "@noble/ed25519": "^1.3.0", + "@stablelib/ed25519": "^1.0.2", + "benchmark": "^2.1.4", + "ed25519": "^0.0.5", + "ed25519-wasm-pro": "^1.1.1", + "node-forge": "^1.0.0", + "supercop.wasm": "^5.0.1" + } +} diff --git a/packages/crypto/benchmark/ephemeral-keys.cjs b/packages/crypto/benchmark/ephemeral-keys.cjs new file mode 100644 index 0000000000..b438489ea6 --- /dev/null +++ b/packages/crypto/benchmark/ephemeral-keys.cjs @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ +const crypto = require('../dist/src/index.js') + +const Benchmark = require('benchmark') + +const suite = new Benchmark.Suite('ephemeral-keys') + +const secrets = [] +const curves = ['P-256', 'P-384', 'P-521'] + +curves.forEach((curve) => { + suite.add(`ephemeral key with secrect ${curve}`, async (d) => { + const res = await crypto.keys.generateEphemeralKeyPair('P-256') + const secret = await res.genSharedKey(res.key) + secrets.push(secret) + d.resolve() + }, { defer: true }) +}) + +suite + .on('cycle', (event) => console.log(String(event.target))) + .run({ async: true }) diff --git a/packages/crypto/benchmark/key-stretcher.cjs b/packages/crypto/benchmark/key-stretcher.cjs new file mode 100644 index 0000000000..0ff11e50ea --- /dev/null +++ b/packages/crypto/benchmark/key-stretcher.cjs @@ -0,0 +1,32 @@ +/* eslint-disable no-console */ +const crypto = require('../dist/src/index.js') + +const Benchmark = require('benchmark') + +const suite = new Benchmark.Suite('key-stretcher') + +const keys = [] + +const ciphers = ['AES-128', 'AES-256', 'Blowfish'] +const hashes = ['SHA1', 'SHA256', 'SHA512'] + +;(async () => { + const res = await crypto.keys.generateEphemeralKeyPair('P-256') + const secret = await res.genSharedKey(res.key) + + ciphers.forEach((cipher) => hashes.forEach((hash) => { + setup(cipher, hash, secret) + })) + + suite + .on('cycle', (event) => console.log(String(event.target))) + .run({ async: true }) +})() + +function setup (cipher, hash, secret) { + suite.add(`keyStretcher ${cipher} ${hash}`, async (d) => { + const k = await crypto.keys.keyStretcher(cipher, hash, secret) + keys.push(k) + d.resolve() + }, { defer: true }) +} diff --git a/packages/crypto/benchmark/rsa.cjs b/packages/crypto/benchmark/rsa.cjs new file mode 100644 index 0000000000..7101a40fa7 --- /dev/null +++ b/packages/crypto/benchmark/rsa.cjs @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +const crypto = require('../dist/src/index.js') + +const Benchmark = require('benchmark') + +const suite = new Benchmark.Suite('rsa') + +const keys = [] +const bits = [1024, 2048, 4096] + +bits.forEach((bit) => { + suite.add(`generateKeyPair ${bit}bits`, async (d) => { + const key = await crypto.keys.generateKeyPair('RSA', bit) + keys.push(key) + d.resolve() + }, { + defer: true + }) +}) + +suite.add('sign and verify', async (d) => { + const key = keys[0] + const text = key.genSecret() + + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + + if (res !== true) { throw new Error('failed to verify') } + d.resolve() +}, { + defer: true +}) + +suite + .on('cycle', (event) => console.log(String(event.target))) + .run({ async: true }) diff --git a/packages/crypto/package.json b/packages/crypto/package.json new file mode 100644 index 0000000000..f9d7831dec --- /dev/null +++ b/packages/crypto/package.json @@ -0,0 +1,119 @@ +{ + "name": "@libp2p/crypto", + "version": "1.0.17", + "description": "Crypto primitives for libp2p", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/crypto#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "crypto", + "libp2p", + "rsa", + "secp256k1" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./aes": { + "types": "./dist/src/aes/index.d.ts", + "import": "./dist/src/aes/index.js" + }, + "./ciphers": { + "types": "./dist/src/ciphers/index.d.ts", + "import": "./dist/src/ciphers/index.js" + }, + "./hmac": { + "types": "./dist/src/hmac/index.d.ts", + "import": "./dist/src/hmac/index.js" + }, + "./keys": { + "types": "./dist/src/keys/index.d.ts", + "import": "./dist/src/keys/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/*.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:webkit": "bash -c '[ \"${CI}\" == \"true\" ] && playwright install-deps'; aegir test -t browser -- --browser webkit", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "generate": "protons ./src/keys/keys.proto" + }, + "dependencies": { + "@libp2p/interface-keys": "^1.0.0", + "@libp2p/interfaces": "^3.0.0", + "@noble/ed25519": "^1.6.0", + "@noble/secp256k1": "^1.5.4", + "multiformats": "^11.0.2", + "node-forge": "^1.1.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@types/mocha": "^10.0.0", + "aegir": "^39.0.10", + "benchmark": "^2.1.4", + "protons": "^7.0.2" + }, + "browser": { + "./dist/src/aes/ciphers.js": "./dist/src/aes/ciphers-browser.js", + "./dist/src/ciphers/aes-gcm.js": "./dist/src/ciphers/aes-gcm.browser.js", + "./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js", + "./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js", + "./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js", + "./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/crypto/src/aes/cipher-mode.ts b/packages/crypto/src/aes/cipher-mode.ts new file mode 100644 index 0000000000..b420deed54 --- /dev/null +++ b/packages/crypto/src/aes/cipher-mode.ts @@ -0,0 +1,15 @@ +import { CodeError } from '@libp2p/interfaces/errors' + +const CIPHER_MODES = { + 16: 'aes-128-ctr', + 32: 'aes-256-ctr' +} + +export function cipherMode (key: Uint8Array): string { + if (key.length === 16 || key.length === 32) { + return CIPHER_MODES[key.length] + } + + const modes = Object.entries(CIPHER_MODES).map(([k, v]) => `${k} (${v})`).join(' / ') + throw new CodeError(`Invalid key length ${key.length} bytes. Must be ${modes}`, 'ERR_INVALID_KEY_LENGTH') +} diff --git a/packages/crypto/src/aes/ciphers-browser.ts b/packages/crypto/src/aes/ciphers-browser.ts new file mode 100644 index 0000000000..88d18d7d95 --- /dev/null +++ b/packages/crypto/src/aes/ciphers-browser.ts @@ -0,0 +1,32 @@ + +import 'node-forge/lib/aes.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +export interface Cipher { + update: (data: Uint8Array) => Uint8Array +} + +export function createCipheriv (mode: any, key: Uint8Array, iv: Uint8Array): Cipher { + const cipher2 = forge.cipher.createCipher('AES-CTR', uint8ArrayToString(key, 'ascii')) + cipher2.start({ iv: uint8ArrayToString(iv, 'ascii') }) + return { + update: (data: Uint8Array) => { + cipher2.update(forge.util.createBuffer(uint8ArrayToString(data, 'ascii'))) + return uint8ArrayFromString(cipher2.output.getBytes(), 'ascii') + } + } +} + +export function createDecipheriv (mode: any, key: Uint8Array, iv: Uint8Array): Cipher { + const cipher2 = forge.cipher.createDecipher('AES-CTR', uint8ArrayToString(key, 'ascii')) + cipher2.start({ iv: uint8ArrayToString(iv, 'ascii') }) + return { + update: (data: Uint8Array) => { + cipher2.update(forge.util.createBuffer(uint8ArrayToString(data, 'ascii'))) + return uint8ArrayFromString(cipher2.output.getBytes(), 'ascii') + } + } +} diff --git a/packages/crypto/src/aes/ciphers.ts b/packages/crypto/src/aes/ciphers.ts new file mode 100644 index 0000000000..c1a2cd74a5 --- /dev/null +++ b/packages/crypto/src/aes/ciphers.ts @@ -0,0 +1,4 @@ +import crypto from 'crypto' + +export const createCipheriv = crypto.createCipheriv +export const createDecipheriv = crypto.createDecipheriv diff --git a/packages/crypto/src/aes/index.ts b/packages/crypto/src/aes/index.ts new file mode 100644 index 0000000000..0023ca5496 --- /dev/null +++ b/packages/crypto/src/aes/index.ts @@ -0,0 +1,25 @@ +import { cipherMode } from './cipher-mode.js' +import * as ciphers from './ciphers.js' + +export interface AESCipher { + encrypt: (data: Uint8Array) => Promise + decrypt: (data: Uint8Array) => Promise +} + +export async function create (key: Uint8Array, iv: Uint8Array): Promise { // eslint-disable-line require-await + const mode = cipherMode(key) + const cipher = ciphers.createCipheriv(mode, key, iv) + const decipher = ciphers.createDecipheriv(mode, key, iv) + + const res: AESCipher = { + async encrypt (data) { // eslint-disable-line require-await + return cipher.update(data) + }, + + async decrypt (data) { // eslint-disable-line require-await + return decipher.update(data) + } + } + + return res +} diff --git a/packages/crypto/src/ciphers/aes-gcm.browser.ts b/packages/crypto/src/ciphers/aes-gcm.browser.ts new file mode 100644 index 0000000000..e238e20be7 --- /dev/null +++ b/packages/crypto/src/ciphers/aes-gcm.browser.ts @@ -0,0 +1,109 @@ +import { concat } from 'uint8arrays/concat' +import { fromString } from 'uint8arrays/from-string' +import webcrypto from '../webcrypto.js' +import type { CreateOptions, AESCipher } from './interface.js' + +// WebKit on Linux does not support deriving a key from an empty PBKDF2 key. +// So, as a workaround, we provide the generated key as a constant. We test that +// this generated key is accurate in test/workaround.spec.ts +// Generated via: +// await crypto.subtle.exportKey('jwk', +// await crypto.subtle.deriveKey( +// { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } }, +// await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']), +// { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']) +// ) +export const derivedEmptyPasswordKey = { alg: 'A128GCM', ext: true, k: 'scm9jmO_4BJAgdwWGVulLg', key_ops: ['encrypt', 'decrypt'], kty: 'oct' } + +// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples + +export function create (opts?: CreateOptions): AESCipher { + const algorithm = opts?.algorithm ?? 'AES-GCM' + let keyLength = opts?.keyLength ?? 16 + const nonceLength = opts?.nonceLength ?? 12 + const digest = opts?.digest ?? 'SHA-256' + const saltLength = opts?.saltLength ?? 16 + const iterations = opts?.iterations ?? 32767 + + const crypto = webcrypto.get() + keyLength *= 8 // Browser crypto uses bits instead of bytes + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to encrypt the data. + */ + async function encrypt (data: Uint8Array, password: string | Uint8Array): Promise { // eslint-disable-line require-await + const salt = crypto.getRandomValues(new Uint8Array(saltLength)) + const nonce = crypto.getRandomValues(new Uint8Array(nonceLength)) + const aesGcm = { name: algorithm, iv: nonce } + + if (typeof password === 'string') { + password = fromString(password) + } + + let cryptoKey: CryptoKey + if (password.length === 0) { + cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt']) + try { + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const runtimeDerivedEmptyPassword = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, runtimeDerivedEmptyPassword, { name: algorithm, length: keyLength }, true, ['encrypt']) + } catch { + cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt']) + } + } else { + // Derive a key using PBKDF2. + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt']) + } + + // Encrypt the string. + const ciphertext = await crypto.subtle.encrypt(aesGcm, cryptoKey, data) + return concat([salt, aesGcm.iv, new Uint8Array(ciphertext)]) + } + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to decrypt the data. The options used to create + * this decryption cipher must be the same as those used to create + * the encryption cipher. + */ + async function decrypt (data: Uint8Array, password: string | Uint8Array): Promise { + const salt = data.subarray(0, saltLength) + const nonce = data.subarray(saltLength, saltLength + nonceLength) + const ciphertext = data.subarray(saltLength + nonceLength) + const aesGcm = { name: algorithm, iv: nonce } + + if (typeof password === 'string') { + password = fromString(password) + } + + let cryptoKey: CryptoKey + if (password.length === 0) { + try { + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const runtimeDerivedEmptyPassword = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, runtimeDerivedEmptyPassword, { name: algorithm, length: keyLength }, true, ['decrypt']) + } catch { + cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['decrypt']) + } + } else { + // Derive the key using PBKDF2. + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt']) + } + + // Decrypt the string. + const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext) + return new Uint8Array(plaintext) + } + + const cipher: AESCipher = { + encrypt, + decrypt + } + + return cipher +} diff --git a/packages/crypto/src/ciphers/aes-gcm.ts b/packages/crypto/src/ciphers/aes-gcm.ts new file mode 100644 index 0000000000..0f217d4cb9 --- /dev/null +++ b/packages/crypto/src/ciphers/aes-gcm.ts @@ -0,0 +1,102 @@ +import crypto from 'crypto' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { CreateOptions, AESCipher } from './interface.js' + +// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples + +export function create (opts?: CreateOptions): AESCipher { + const algorithm = opts?.algorithm ?? 'aes-128-gcm' + const keyLength = opts?.keyLength ?? 16 + const nonceLength = opts?.nonceLength ?? 12 + const digest = opts?.digest ?? 'sha256' + const saltLength = opts?.saltLength ?? 16 + const iterations = opts?.iterations ?? 32767 + const algorithmTagLength = opts?.algorithmTagLength ?? 16 + + async function encryptWithKey (data: Uint8Array, key: Uint8Array): Promise { // eslint-disable-line require-await + const nonce = crypto.randomBytes(nonceLength) + + // Create the cipher instance. + const cipher = crypto.createCipheriv(algorithm, key, nonce) + + // Encrypt and prepend nonce. + const ciphertext = uint8ArrayConcat([cipher.update(data), cipher.final()]) + + // @ts-expect-error getAuthTag is not a function + return uint8ArrayConcat([nonce, ciphertext, cipher.getAuthTag()]) + } + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to encrypt the data. + */ + async function encrypt (data: Uint8Array, password: string | Uint8Array): Promise { // eslint-disable-line require-await + // Generate a 128-bit salt using a CSPRNG. + const salt = crypto.randomBytes(saltLength) + + if (typeof password === 'string') { + password = uint8ArrayFromString(password) + } + + // Derive a key using PBKDF2. + const key = crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest) + + // Encrypt and prepend salt. + return uint8ArrayConcat([salt, await encryptWithKey(Uint8Array.from(data), key)]) + } + + /** + * Decrypts the given cipher text with the provided key. The `key` should + * be a cryptographically safe key and not a plaintext password. To use + * a plaintext password, use `decrypt`. The options used to create + * this decryption cipher must be the same as those used to create + * the encryption cipher. + */ + async function decryptWithKey (ciphertextAndNonce: Uint8Array, key: Uint8Array): Promise { // eslint-disable-line require-await + // Create Uint8Arrays of nonce, ciphertext and tag. + const nonce = ciphertextAndNonce.subarray(0, nonceLength) + const ciphertext = ciphertextAndNonce.subarray(nonceLength, ciphertextAndNonce.length - algorithmTagLength) + const tag = ciphertextAndNonce.subarray(ciphertext.length + nonceLength) + + // Create the cipher instance. + const cipher = crypto.createDecipheriv(algorithm, key, nonce) + + // Decrypt and return result. + // @ts-expect-error getAuthTag is not a function + cipher.setAuthTag(tag) + return uint8ArrayConcat([cipher.update(ciphertext), cipher.final()]) + } + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to decrypt the data. The options used to create + * this decryption cipher must be the same as those used to create + * the encryption cipher. + * + * @param {Uint8Array} data - The data to decrypt + * @param {string|Uint8Array} password - A plain password + */ + async function decrypt (data: Uint8Array, password: string | Uint8Array): Promise { // eslint-disable-line require-await + // Create Uint8Arrays of salt and ciphertextAndNonce. + const salt = data.subarray(0, saltLength) + const ciphertextAndNonce = data.subarray(saltLength) + + if (typeof password === 'string') { + password = uint8ArrayFromString(password) + } + + // Derive the key using PBKDF2. + const key = crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest) + + // Decrypt and return result. + return decryptWithKey(ciphertextAndNonce, key) + } + + const cipher: AESCipher = { + encrypt, + decrypt + } + + return cipher +} diff --git a/packages/crypto/src/ciphers/interface.ts b/packages/crypto/src/ciphers/interface.ts new file mode 100644 index 0000000000..08ce7b6b86 --- /dev/null +++ b/packages/crypto/src/ciphers/interface.ts @@ -0,0 +1,15 @@ + +export interface CreateOptions { + algorithm?: string + nonceLength?: number + keyLength?: number + digest?: string + saltLength?: number + iterations?: number + algorithmTagLength?: number +} + +export interface AESCipher { + 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 new file mode 100644 index 0000000000..0a5d91fd5c --- /dev/null +++ b/packages/crypto/src/hmac/index-browser.ts @@ -0,0 +1,35 @@ +import webcrypto from '../webcrypto.js' +import lengths from './lengths.js' + +const hashTypes = { + SHA1: 'SHA-1', + SHA256: 'SHA-256', + SHA512: 'SHA-512' +} + +const sign = async (key: CryptoKey, data: Uint8Array): Promise => { + const buf = await webcrypto.get().subtle.sign({ name: 'HMAC' }, key, data) + return new Uint8Array(buf, 0, buf.byteLength) +} + +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( + 'raw', + secret, + { + name: 'HMAC', + hash: { name: hash } + }, + false, + ['sign'] + ) + + return { + async digest (data: Uint8Array) { // eslint-disable-line require-await + return sign(key, data) + }, + length: lengths[hashType] + } +} diff --git a/packages/crypto/src/hmac/index.ts b/packages/crypto/src/hmac/index.ts new file mode 100644 index 0000000000..a0a802f13c --- /dev/null +++ b/packages/crypto/src/hmac/index.ts @@ -0,0 +1,20 @@ +import crypto from 'crypto' +import lengths from './lengths.js' + +export interface HMAC { + digest: (data: Uint8Array) => Promise + length: number +} + +export async function create (hash: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise { + const res = { + async digest (data: Uint8Array) { // eslint-disable-line require-await + const hmac = crypto.createHmac(hash.toLowerCase(), secret) + hmac.update(data) + return hmac.digest() + }, + length: lengths[hash] + } + + return res +} diff --git a/packages/crypto/src/hmac/lengths.ts b/packages/crypto/src/hmac/lengths.ts new file mode 100644 index 0000000000..396bafc780 --- /dev/null +++ b/packages/crypto/src/hmac/lengths.ts @@ -0,0 +1,6 @@ + +export default { + SHA1: 20, + SHA256: 32, + SHA512: 64 +} diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts new file mode 100644 index 0000000000..9e368358eb --- /dev/null +++ b/packages/crypto/src/index.ts @@ -0,0 +1,11 @@ +import * as aes from './aes/index.js' +import * as hmac from './hmac/index.js' +import * as keys from './keys/index.js' +import pbkdf2 from './pbkdf2.js' +import randomBytes from './random-bytes.js' + +export { aes } +export { hmac } +export { keys } +export { randomBytes } +export { pbkdf2 } diff --git a/packages/crypto/src/keys/ecdh-browser.ts b/packages/crypto/src/keys/ecdh-browser.ts new file mode 100644 index 0000000000..c371429f33 --- /dev/null +++ b/packages/crypto/src/keys/ecdh-browser.ts @@ -0,0 +1,137 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { base64urlToBuffer } from '../util.js' +import webcrypto from '../webcrypto.js' +import type { ECDHKey, ECDHKeyPair, JWKEncodedPrivateKey, JWKEncodedPublicKey } from './interface.js' + +const bits = { + 'P-256': 256, + 'P-384': 384, + 'P-521': 521 +} + +const curveTypes = Object.keys(bits) +const names = curveTypes.join(' / ') + +export async function generateEphmeralKeyPair (curve: string): Promise { + if (curve !== 'P-256' && curve !== 'P-384' && curve !== 'P-521') { + throw new CodeError(`Unknown curve: ${curve}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const pair = await webcrypto.get().subtle.generateKey( + { + name: 'ECDH', + namedCurve: curve + }, + true, + ['deriveBits'] + ) + + // forcePrivate is used for testing only + const genSharedKey = async (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair): Promise => { + let privateKey + + if (forcePrivate != null) { + privateKey = await webcrypto.get().subtle.importKey( + 'jwk', + unmarshalPrivateKey(curve, forcePrivate), + { + name: 'ECDH', + namedCurve: curve + }, + false, + ['deriveBits'] + ) + } else { + privateKey = pair.privateKey + } + + const key = await webcrypto.get().subtle.importKey( + 'jwk', + unmarshalPublicKey(curve, theirPub), + { + name: 'ECDH', + namedCurve: curve + }, + false, + [] + ) + + const buffer = await webcrypto.get().subtle.deriveBits( + { + name: 'ECDH', + // @ts-expect-error namedCurve is missing from the types + namedCurve: curve, + public: key + }, + privateKey, + bits[curve] + ) + + return new Uint8Array(buffer, 0, buffer.byteLength) + } + + const publicKey = await webcrypto.get().subtle.exportKey('jwk', pair.publicKey) + + const ecdhKey: ECDHKey = { + key: marshalPublicKey(publicKey), + genSharedKey + } + + return ecdhKey +} + +const curveLengths = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 +} + +// Marshal converts a jwk encoded ECDH public key into the +// form specified in section 4.3.6 of ANSI X9.62. (This is the format +// go-ipfs uses) +function marshalPublicKey (jwk: JsonWebKey): Uint8Array { + if (jwk.crv == null || jwk.x == null || jwk.y == null) { + throw new CodeError('JWK was missing components', 'ERR_INVALID_PARAMETERS') + } + + if (jwk.crv !== 'P-256' && jwk.crv !== 'P-384' && jwk.crv !== 'P-521') { + throw new CodeError(`Unknown curve: ${jwk.crv}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const byteLen = curveLengths[jwk.crv] + + return uint8ArrayConcat([ + Uint8Array.from([4]), // uncompressed point + base64urlToBuffer(jwk.x, byteLen), + base64urlToBuffer(jwk.y, byteLen) + ], 1 + byteLen * 2) +} + +// Unmarshal converts a point, serialized by Marshal, into an jwk encoded key +function unmarshalPublicKey (curve: string, key: Uint8Array): JWKEncodedPublicKey { + if (curve !== 'P-256' && curve !== 'P-384' && curve !== 'P-521') { + throw new CodeError(`Unknown curve: ${curve}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const byteLen = curveLengths[curve] + + if (!uint8ArrayEquals(key.subarray(0, 1), Uint8Array.from([4]))) { + throw new CodeError('Cannot unmarshal public key - invalid key format', 'ERR_INVALID_KEY_FORMAT') + } + + return { + kty: 'EC', + crv: curve, + x: uint8ArrayToString(key.subarray(1, byteLen + 1), 'base64url'), + y: uint8ArrayToString(key.subarray(1 + byteLen), 'base64url'), + ext: true + } +} + +const unmarshalPrivateKey = (curve: string, key: ECDHKeyPair): JWKEncodedPrivateKey => ({ + ...unmarshalPublicKey(curve, key.public), + d: uint8ArrayToString(key.private, 'base64url') +}) diff --git a/packages/crypto/src/keys/ecdh.ts b/packages/crypto/src/keys/ecdh.ts new file mode 100644 index 0000000000..5e139f4e08 --- /dev/null +++ b/packages/crypto/src/keys/ecdh.ts @@ -0,0 +1,33 @@ +import crypto from 'crypto' +import { CodeError } from '@libp2p/interfaces/errors' +import type { ECDHKey, ECDHKeyPair } from './interface.js' + +const curves = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1' +} + +const curveTypes = Object.keys(curves) +const names = curveTypes.join(' / ') + +export async function generateEphmeralKeyPair (curve: string): Promise { // eslint-disable-line require-await + if (curve !== 'P-256' && curve !== 'P-384' && curve !== 'P-521') { + throw new CodeError(`Unknown curve: ${curve}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const ecdh = crypto.createECDH(curves[curve]) + ecdh.generateKeys() + + return { + key: ecdh.getPublicKey() as Uint8Array, + + async genSharedKey (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair): Promise { // eslint-disable-line require-await + if (forcePrivate != null) { + ecdh.setPrivateKey(forcePrivate.private) + } + + return ecdh.computeSecret(theirPub) + } + } +} diff --git a/packages/crypto/src/keys/ed25519-browser.ts b/packages/crypto/src/keys/ed25519-browser.ts new file mode 100644 index 0000000000..8d2f6de154 --- /dev/null +++ b/packages/crypto/src/keys/ed25519-browser.ts @@ -0,0 +1,64 @@ +import * as ed from '@noble/ed25519' +import type { Uint8ArrayKeyPair } from './interface' + +const PUBLIC_KEY_BYTE_LENGTH = 32 +const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys +const KEYS_BYTE_LENGTH = 32 + +export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength } +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +export async function generateKey (): Promise { + // the actual private key (32 bytes) + const privateKeyRaw = ed.utils.randomPrivateKey() + const publicKey = await ed.getPublicKey(privateKeyRaw) + + // concatenated the public key to the private key + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +/** + * Generate keypair from a 32 byte uint8array + */ +export async function generateKeyFromSeed (seed: Uint8Array): Promise { + if (seed.length !== KEYS_BYTE_LENGTH) { + throw new TypeError('"seed" must be 32 bytes in length.') + } else if (!(seed instanceof Uint8Array)) { + throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.') + } + + // based on node forges algorithm, the seed is used directly as private key + const privateKeyRaw = seed + const publicKey = await ed.getPublicKey(privateKeyRaw) + + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array): Promise { + const privateKeyRaw = privateKey.subarray(0, KEYS_BYTE_LENGTH) + + return ed.sign(msg, privateKeyRaw) +} + +export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise { + return ed.verify(sig, msg, publicKey) +} + +function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array { + const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH) + for (let i = 0; i < KEYS_BYTE_LENGTH; i++) { + privateKey[i] = privateKeyRaw[i] + privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i] + } + return privateKey +} diff --git a/packages/crypto/src/keys/ed25519-class.ts b/packages/crypto/src/keys/ed25519-class.ts new file mode 100644 index 0000000000..baae1bc188 --- /dev/null +++ b/packages/crypto/src/keys/ed25519-class.ts @@ -0,0 +1,146 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { base58btc } from 'multiformats/bases/base58' +import { identity } from 'multiformats/hashes/identity' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import * as crypto from './ed25519.js' +import { exporter } from './exporter.js' +import * as pbm from './keys.js' +import type { Multibase } from 'multiformats' + +export class Ed25519PublicKey { + private readonly _key: Uint8Array + + constructor (key: Uint8Array) { + this._key = ensureKey(key, crypto.publicKeyLength) + } + + async verify (data: Uint8Array, sig: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal (): Uint8Array { + return this._key + } + + get bytes (): Uint8Array { + return pbm.PublicKey.encode({ + Type: pbm.KeyType.Ed25519, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } +} + +export class Ed25519PrivateKey { + private readonly _key: Uint8Array + private readonly _publicKey: Uint8Array + + // key - 64 byte Uint8Array containing private key + // publicKey - 32 byte Uint8Array containing public key + constructor (key: Uint8Array, publicKey: Uint8Array) { + this._key = ensureKey(key, crypto.privateKeyLength) + this._publicKey = ensureKey(publicKey, crypto.publicKeyLength) + } + + async sign (message: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndSign(this._key, message) + } + + get public (): Ed25519PublicKey { + return new Ed25519PublicKey(this._publicKey) + } + + marshal (): Uint8Array { + return this._key + } + + get bytes (): Uint8Array { + return pbm.PrivateKey.encode({ + Type: pbm.KeyType.Ed25519, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the identity multihash containing its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + * + * @returns {Promise} + */ + async id (): Promise { + const encoding = identity.digest(this.public.bytes) + return base58btc.encode(encoding.bytes).substring(1) + } + + /** + * Exports the key into a password protected `format` + */ + async export (password: string, format = 'libp2p-key'): Promise> { + if (format === 'libp2p-key') { + return exporter(this.bytes, password) + } else { + throw new CodeError(`export format '${format}' is not supported`, 'ERR_INVALID_EXPORT_FORMAT') + } + } +} + +export function unmarshalEd25519PrivateKey (bytes: Uint8Array): Ed25519PrivateKey { + // Try the old, redundant public key version + if (bytes.length > crypto.privateKeyLength) { + bytes = ensureKey(bytes, crypto.privateKeyLength + crypto.publicKeyLength) + const privateKeyBytes = bytes.subarray(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.subarray(crypto.privateKeyLength, bytes.length) + return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) + } + + bytes = ensureKey(bytes, crypto.privateKeyLength) + const privateKeyBytes = bytes.subarray(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.subarray(crypto.publicKeyLength) + return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) +} + +export function unmarshalEd25519PublicKey (bytes: Uint8Array): Ed25519PublicKey { + bytes = ensureKey(bytes, crypto.publicKeyLength) + return new Ed25519PublicKey(bytes) +} + +export async function generateKeyPair (): Promise { + const { privateKey, publicKey } = await crypto.generateKey() + return new Ed25519PrivateKey(privateKey, publicKey) +} + +export async function generateKeyPairFromSeed (seed: Uint8Array): Promise { + const { privateKey, publicKey } = await crypto.generateKeyFromSeed(seed) + return new Ed25519PrivateKey(privateKey, publicKey) +} + +function ensureKey (key: Uint8Array, length: number): Uint8Array { + key = Uint8Array.from(key ?? []) + if (key.length !== length) { + throw new CodeError(`Key must be a Uint8Array of length ${length}, got ${key.length}`, 'ERR_INVALID_KEY_TYPE') + } + return key +} diff --git a/packages/crypto/src/keys/ed25519.ts b/packages/crypto/src/keys/ed25519.ts new file mode 100644 index 0000000000..0de68a526d --- /dev/null +++ b/packages/crypto/src/keys/ed25519.ts @@ -0,0 +1,137 @@ +import crypto from 'crypto' +import { promisify } from 'util' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { toString as uint8arrayToString } from 'uint8arrays/to-string' +import type { Uint8ArrayKeyPair } from './interface.js' + +const keypair = promisify(crypto.generateKeyPair) + +const PUBLIC_KEY_BYTE_LENGTH = 32 +const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys +const KEYS_BYTE_LENGTH = 32 +const SIGNATURE_BYTE_LENGTH = 64 + +export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength } +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +function derivePublicKey (privateKey: Uint8Array): Uint8Array { + const keyObject = crypto.createPrivateKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + x: '', + d: uint8arrayToString(privateKey, 'base64url'), + kty: 'OKP' + } + }) + const jwk = keyObject.export({ + format: 'jwk' + }) + + if (jwk.x == null || jwk.x === '') { + throw new Error('Could not export JWK public key') + } + + return uint8arrayFromString(jwk.x, 'base64url') +} + +export async function generateKey (): Promise { + const key = await keypair('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'jwk' }, + privateKeyEncoding: { type: 'pkcs8', format: 'jwk' } + }) + + // @ts-expect-error node types are missing jwk as a format + const privateKeyRaw = uint8arrayFromString(key.privateKey.d, 'base64url') + // @ts-expect-error node types are missing jwk as a format + const publicKeyRaw = uint8arrayFromString(key.privateKey.x, 'base64url') + + return { + privateKey: concatKeys(privateKeyRaw, publicKeyRaw), + publicKey: publicKeyRaw + } +} + +/** + * Generate keypair from a 32 byte uint8array + */ +export async function generateKeyFromSeed (seed: Uint8Array): Promise { + if (seed.length !== KEYS_BYTE_LENGTH) { + throw new TypeError('"seed" must be 32 bytes in length.') + } else if (!(seed instanceof Uint8Array)) { + throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.') + } + + // based on node forges algorithm, the seed is used directly as private key + const publicKeyRaw = derivePublicKey(seed) + + return { + privateKey: concatKeys(seed, publicKeyRaw), + publicKey: publicKeyRaw + } +} + +export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise { + if (!(key instanceof Uint8Array)) { + throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.') + } + + let privateKey: Uint8Array + let publicKey: Uint8Array + + if (key.byteLength === PRIVATE_KEY_BYTE_LENGTH) { + privateKey = key.subarray(0, 32) + publicKey = key.subarray(32) + } else if (key.byteLength === KEYS_BYTE_LENGTH) { + privateKey = key.subarray(0, 32) + publicKey = derivePublicKey(privateKey) + } else { + throw new TypeError('"key" must be 64 or 32 bytes in length.') + } + + const obj = crypto.createPrivateKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + d: uint8arrayToString(privateKey, 'base64url'), + x: uint8arrayToString(publicKey, 'base64url'), + kty: 'OKP' + } + }) + + return crypto.sign(null, msg, obj) +} + +export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise { + if (key.byteLength !== PUBLIC_KEY_BYTE_LENGTH) { + throw new TypeError('"key" must be 32 bytes in length.') + } else if (!(key instanceof Uint8Array)) { + throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.') + } + + if (sig.byteLength !== SIGNATURE_BYTE_LENGTH) { + throw new TypeError('"sig" must be 64 bytes in length.') + } else if (!(sig instanceof Uint8Array)) { + throw new TypeError('"sig" must be a node.js Buffer, or Uint8Array.') + } + + const obj = crypto.createPublicKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + x: uint8arrayToString(key, 'base64url'), + kty: 'OKP' + } + }) + + return crypto.verify(null, msg, obj, sig) +} + +function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array { + const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH) + for (let i = 0; i < KEYS_BYTE_LENGTH; i++) { + privateKey[i] = privateKeyRaw[i] + privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i] + } + return privateKey +} diff --git a/packages/crypto/src/keys/ephemeral-keys.ts b/packages/crypto/src/keys/ephemeral-keys.ts new file mode 100644 index 0000000000..f60cc0f1a8 --- /dev/null +++ b/packages/crypto/src/keys/ephemeral-keys.ts @@ -0,0 +1,9 @@ +import { generateEphmeralKeyPair } from './ecdh.js' + +/** + * Generates an ephemeral public key and returns a function that will compute + * the shared secret key. + * + * Focuses only on ECDH now, but can be made more general in the future. + */ +export default generateEphmeralKeyPair diff --git a/packages/crypto/src/keys/exporter.ts b/packages/crypto/src/keys/exporter.ts new file mode 100644 index 0000000000..db62943f31 --- /dev/null +++ b/packages/crypto/src/keys/exporter.ts @@ -0,0 +1,14 @@ +import { base64 } from 'multiformats/bases/base64' +import * as ciphers from '../ciphers/aes-gcm.js' +import type { Multibase } from 'multiformats' + +/** + * Exports the given PrivateKey as a base64 encoded string. + * The PrivateKey is encrypted via a password derived PBKDF2 key + * leveraging the aes-gcm cipher algorithm. + */ +export async function exporter (privateKey: Uint8Array, password: string): Promise> { + const cipher = ciphers.create() + const encryptedKey = await cipher.encrypt(privateKey, password) + return base64.encode(encryptedKey) +} diff --git a/packages/crypto/src/keys/importer.ts b/packages/crypto/src/keys/importer.ts new file mode 100644 index 0000000000..d26b0226c7 --- /dev/null +++ b/packages/crypto/src/keys/importer.ts @@ -0,0 +1,13 @@ +import { base64 } from 'multiformats/bases/base64' +import * as ciphers from '../ciphers/aes-gcm.js' + +/** + * Attempts to decrypt a base64 encoded PrivateKey string + * with the given password. The privateKey must have been exported + * using the same password and underlying cipher (aes-gcm) + */ +export async function importer (privateKey: string, password: string): Promise { + const encryptedKey = base64.decode(privateKey) + const cipher = ciphers.create() + return cipher.decrypt(encryptedKey, password) +} diff --git a/packages/crypto/src/keys/index.ts b/packages/crypto/src/keys/index.ts new file mode 100644 index 0000000000..7a15d0d480 --- /dev/null +++ b/packages/crypto/src/keys/index.ts @@ -0,0 +1,129 @@ +import 'node-forge/lib/asn1.js' +import 'node-forge/lib/pbe.js' +import { CodeError } from '@libp2p/interfaces/errors' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as Ed25519 from './ed25519-class.js' +import generateEphemeralKeyPair from './ephemeral-keys.js' +import { importer } from './importer.js' +import { keyStretcher } from './key-stretcher.js' +import * as keysPBM from './keys.js' +import * as RSA from './rsa-class.js' +import * as Secp256k1 from './secp256k1-class.js' +import type { PrivateKey, PublicKey } from '@libp2p/interface-keys' + +export { keyStretcher } +export { generateEphemeralKeyPair } +export { keysPBM } + +export type KeyTypes = 'RSA' | 'Ed25519' | 'secp256k1' + +export const supportedKeys = { + rsa: RSA, + ed25519: Ed25519, + secp256k1: Secp256k1 +} + +function unsupportedKey (type: string): CodeError> { + const supported = Object.keys(supportedKeys).join(' / ') + return new CodeError(`invalid or unsupported key type ${type}. Must be ${supported}`, 'ERR_UNSUPPORTED_KEY_TYPE') +} + +function typeToKey (type: string): typeof RSA | typeof Ed25519 | typeof Secp256k1 { + type = type.toLowerCase() + + if (type === 'rsa' || type === 'ed25519' || type === 'secp256k1') { + return supportedKeys[type] + } + + throw unsupportedKey(type) +} + +// Generates a keypair of the given type and bitsize +export async function generateKeyPair (type: KeyTypes, bits?: number): Promise { // eslint-disable-line require-await + return typeToKey(type).generateKeyPair(bits ?? 2048) +} + +// Generates a keypair of the given type and bitsize +// seed is a 32 byte uint8array +export async function generateKeyPairFromSeed (type: KeyTypes, seed: Uint8Array, bits?: number): Promise { // eslint-disable-line require-await + if (type.toLowerCase() !== 'ed25519') { + throw new CodeError('Seed key derivation is unimplemented for RSA or secp256k1', 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE') + } + + return Ed25519.generateKeyPairFromSeed(seed) +} + +// Converts a protobuf serialized public key into its +// representative object +export function unmarshalPublicKey (buf: Uint8Array): PublicKey { + const decoded = keysPBM.PublicKey.decode(buf) + const data = decoded.Data ?? new Uint8Array() + + switch (decoded.Type) { + case keysPBM.KeyType.RSA: + return supportedKeys.rsa.unmarshalRsaPublicKey(data) + case keysPBM.KeyType.Ed25519: + return supportedKeys.ed25519.unmarshalEd25519PublicKey(data) + case keysPBM.KeyType.Secp256k1: + return supportedKeys.secp256k1.unmarshalSecp256k1PublicKey(data) + default: + throw unsupportedKey(decoded.Type ?? 'RSA') + } +} + +// Converts a public key object into a protobuf serialized public key +export function marshalPublicKey (key: { bytes: Uint8Array }, type?: string): Uint8Array { + type = (type ?? 'rsa').toLowerCase() + typeToKey(type) // check type + return key.bytes +} + +// Converts a protobuf serialized private key into its +// representative object +export async function unmarshalPrivateKey (buf: Uint8Array): Promise { // eslint-disable-line require-await + const decoded = keysPBM.PrivateKey.decode(buf) + const data = decoded.Data ?? new Uint8Array() + + switch (decoded.Type) { + case keysPBM.KeyType.RSA: + return supportedKeys.rsa.unmarshalRsaPrivateKey(data) + case keysPBM.KeyType.Ed25519: + return supportedKeys.ed25519.unmarshalEd25519PrivateKey(data) + case keysPBM.KeyType.Secp256k1: + return supportedKeys.secp256k1.unmarshalSecp256k1PrivateKey(data) + default: + throw unsupportedKey(decoded.Type ?? 'RSA') + } +} + +// Converts a private key object into a protobuf serialized private key +export function marshalPrivateKey (key: { bytes: Uint8Array }, type?: string): Uint8Array { + type = (type ?? 'rsa').toLowerCase() + typeToKey(type) // check type + return key.bytes +} + +/** + * + * @param {string} encryptedKey + * @param {string} password + */ +export async function importKey (encryptedKey: string, password: string): Promise { // eslint-disable-line require-await + try { + const key = await importer(encryptedKey, password) + return await unmarshalPrivateKey(key) + } catch (_) { + // Ignore and try the old pem decrypt + } + + // Only rsa supports pem right now + const key = forge.pki.decryptRsaPrivateKey(encryptedKey, password) + if (key === null) { + throw new CodeError('Cannot read the key, most likely the password is wrong or not a RSA key', 'ERR_CANNOT_DECRYPT_PEM') + } + let der = forge.asn1.toDer(forge.pki.privateKeyToAsn1(key)) + der = uint8ArrayFromString(der.getBytes(), 'ascii') + return supportedKeys.rsa.unmarshalRsaPrivateKey(der) +} diff --git a/packages/crypto/src/keys/interface.ts b/packages/crypto/src/keys/interface.ts new file mode 100644 index 0000000000..30bceb1dd2 --- /dev/null +++ b/packages/crypto/src/keys/interface.ts @@ -0,0 +1,35 @@ + +export interface JWKKeyPair { + privateKey: JsonWebKey + publicKey: JsonWebKey +} + +export interface Uint8ArrayKeyPair { + privateKey: Uint8Array + publicKey: Uint8Array +} + +export interface ECDHKeyPair { + private: Uint8Array + public: Uint8Array +} + +export interface ECDHKey { + key: Uint8Array + genSharedKey: (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair) => Promise +} + +export interface JWKEncodedPublicKey { kty: string, crv: 'P-256' | 'P-384' | 'P-521', x: string, y: string, ext: boolean } + +export interface JWKEncodedPrivateKey extends JWKEncodedPublicKey { d: string} + +export interface EnhancedKey { + iv: Uint8Array + cipherKey: Uint8Array + macKey: Uint8Array +} + +export interface EnhancedKeyPair { + k1: EnhancedKey + k2: EnhancedKey +} diff --git a/packages/crypto/src/keys/jwk2pem.ts b/packages/crypto/src/keys/jwk2pem.ts new file mode 100644 index 0000000000..64feebc188 --- /dev/null +++ b/packages/crypto/src/keys/jwk2pem.ts @@ -0,0 +1,21 @@ +import 'node-forge/lib/rsa.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { base64urlToBigInteger } from '../util.js' + +export interface JWK { + encrypt: (msg: string) => string + decrypt: (msg: string) => string +} + +function convert (key: any, types: string[]): Array { + return types.map(t => base64urlToBigInteger(key[t])) +} + +export function jwk2priv (key: JsonWebKey): JWK { + return forge.pki.setRsaPrivateKey(...convert(key, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'])) +} + +export function jwk2pub (key: JsonWebKey): JWK { + return forge.pki.setRsaPublicKey(...convert(key, ['n', 'e'])) +} diff --git a/packages/crypto/src/keys/key-stretcher.ts b/packages/crypto/src/keys/key-stretcher.ts new file mode 100644 index 0000000000..c1feefcf0b --- /dev/null +++ b/packages/crypto/src/keys/key-stretcher.ts @@ -0,0 +1,78 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as hmac from '../hmac/index.js' +import type { EnhancedKey, EnhancedKeyPair } from './interface.js' + +const cipherMap = { + 'AES-128': { + ivSize: 16, + keySize: 16 + }, + 'AES-256': { + ivSize: 16, + keySize: 32 + }, + Blowfish: { + ivSize: 8, + keySize: 32 + } +} + +/** + * Generates a set of keys for each party by stretching the shared key. + * (myIV, theirIV, myCipherKey, theirCipherKey, myMACKey, theirMACKey) + */ +export async function keyStretcher (cipherType: 'AES-128' | 'AES-256' | 'Blowfish', hash: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise { + const cipher = cipherMap[cipherType] + + if (cipher == null) { + const allowed = Object.keys(cipherMap).join(' / ') + throw new CodeError(`unknown cipher type '${cipherType}'. Must be ${allowed}`, 'ERR_INVALID_CIPHER_TYPE') + } + + if (hash == null) { + throw new CodeError('missing hash type', 'ERR_MISSING_HASH_TYPE') + } + + const cipherKeySize = cipher.keySize + const ivSize = cipher.ivSize + const hmacKeySize = 20 + const seed = uint8ArrayFromString('key expansion') + const resultLength = 2 * (ivSize + cipherKeySize + hmacKeySize) + + const m = await hmac.create(hash, secret) + let a = await m.digest(seed) + + const result = [] + let j = 0 + + while (j < resultLength) { + const b = await m.digest(uint8ArrayConcat([a, seed])) + let todo = b.length + + if (j + todo > resultLength) { + todo = resultLength - j + } + + result.push(b) + j += todo + a = await m.digest(a) + } + + const half = resultLength / 2 + const resultBuffer = uint8ArrayConcat(result) + const r1 = resultBuffer.subarray(0, half) + const r2 = resultBuffer.subarray(half, resultLength) + + const createKey = (res: Uint8Array): EnhancedKey => ({ + iv: res.subarray(0, ivSize), + cipherKey: res.subarray(ivSize, ivSize + cipherKeySize), + macKey: res.subarray(ivSize + cipherKeySize) + }) + + return { + k1: createKey(r1), + k2: createKey(r2) + } +} diff --git a/packages/crypto/src/keys/keys.proto b/packages/crypto/src/keys/keys.proto new file mode 100644 index 0000000000..7f49971654 --- /dev/null +++ b/packages/crypto/src/keys/keys.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +enum KeyType { + RSA = 0; + Ed25519 = 1; + Secp256k1 = 2; +} +message PublicKey { + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional KeyType Type = 1; + + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional bytes Data = 2; +} +message PrivateKey { + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional KeyType Type = 1; + + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional bytes Data = 2; +} diff --git a/packages/crypto/src/keys/keys.ts b/packages/crypto/src/keys/keys.ts new file mode 100644 index 0000000000..0d9fcd41ed --- /dev/null +++ b/packages/crypto/src/keys/keys.ts @@ -0,0 +1,156 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export enum KeyType { + RSA = 'RSA', + Ed25519 = 'Ed25519', + Secp256k1 = 'Secp256k1' +} + +enum __KeyTypeValues { + RSA = 0, + Ed25519 = 1, + Secp256k1 = 2 +} + +export namespace KeyType { + export const codec = (): Codec => { + return enumeration(__KeyTypeValues) + } +} +export interface PublicKey { + Type?: KeyType + Data?: Uint8Array +} + +export namespace PublicKey { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.Type != null) { + w.uint32(8) + KeyType.codec().encode(obj.Type, w) + } + + if (obj.Data != null) { + w.uint32(18) + w.bytes(obj.Data) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.Type = KeyType.codec().decode(reader) + break + case 2: + obj.Data = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PublicKey.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PublicKey => { + return decodeMessage(buf, PublicKey.codec()) + } +} + +export interface PrivateKey { + Type?: KeyType + Data?: Uint8Array +} + +export namespace PrivateKey { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.Type != null) { + w.uint32(8) + KeyType.codec().encode(obj.Type, w) + } + + if (obj.Data != null) { + w.uint32(18) + w.bytes(obj.Data) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.Type = KeyType.codec().decode(reader) + break + case 2: + obj.Data = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PrivateKey.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PrivateKey => { + return decodeMessage(buf, PrivateKey.codec()) + } +} diff --git a/packages/crypto/src/keys/rsa-browser.ts b/packages/crypto/src/keys/rsa-browser.ts new file mode 100644 index 0000000000..a17eae884c --- /dev/null +++ b/packages/crypto/src/keys/rsa-browser.ts @@ -0,0 +1,157 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import randomBytes from '../random-bytes.js' +import webcrypto from '../webcrypto.js' +import { jwk2pub, jwk2priv } from './jwk2pem.js' +import * as utils from './rsa-utils.js' +import type { JWKKeyPair } from './interface.js' + +export { utils } + +export async function generateKey (bits: number): Promise { + const pair = await webcrypto.get().subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: bits, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: 'SHA-256' } + }, + true, + ['sign', 'verify'] + ) + + const keys = await exportKey(pair) + + return { + privateKey: keys[0], + publicKey: keys[1] + } +} + +// Takes a jwk key +export async function unmarshalPrivateKey (key: JsonWebKey): Promise { + const privateKey = await webcrypto.get().subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + true, + ['sign'] + ) + + const pair = [ + privateKey, + await derivePublicFromPrivate(key) + ] + + const keys = await exportKey({ + privateKey: pair[0], + publicKey: pair[1] + }) + + return { + privateKey: keys[0], + publicKey: keys[1] + } +} + +export { randomBytes as getRandomValues } + +export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise { + const privateKey = await webcrypto.get().subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + false, + ['sign'] + ) + + const sig = await webcrypto.get().subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + privateKey, + Uint8Array.from(msg) + ) + + return new Uint8Array(sig, 0, sig.byteLength) +} + +export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise { + const publicKey = await webcrypto.get().subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + false, + ['verify'] + ) + + return webcrypto.get().subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + publicKey, + sig, + msg + ) +} + +async function exportKey (pair: CryptoKeyPair): Promise<[JsonWebKey, JsonWebKey]> { + if (pair.privateKey == null || pair.publicKey == null) { + throw new CodeError('Private and public key are required', 'ERR_INVALID_PARAMETERS') + } + + return Promise.all([ + webcrypto.get().subtle.exportKey('jwk', pair.privateKey), + webcrypto.get().subtle.exportKey('jwk', pair.publicKey) + ]) +} + +async function derivePublicFromPrivate (jwKey: JsonWebKey): Promise { + return webcrypto.get().subtle.importKey( + 'jwk', + { + kty: jwKey.kty, + n: jwKey.n, + e: jwKey.e + }, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + true, + ['verify'] + ) +} + +/* + +RSA encryption/decryption for the browser with webcrypto workaround +"bloody dark magic. webcrypto's why." + +Explanation: + - Convert JWK to nodeForge + - Convert msg Uint8Array to nodeForge buffer: ByteBuffer is a "binary-string backed buffer", so let's make our Uint8Array a binary string + - Convert resulting nodeForge buffer to Uint8Array: it returns a binary string, turn that into a 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) + return uint8ArrayFromString(fomsg, 'ascii') +} + +export function encrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array { + return convertKey(key, true, msg, (msg, key) => key.encrypt(msg)) +} + +export function decrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array { + return convertKey(key, false, msg, (msg, key) => key.decrypt(msg)) +} diff --git a/packages/crypto/src/keys/rsa-class.ts b/packages/crypto/src/keys/rsa-class.ts new file mode 100644 index 0000000000..dd25257037 --- /dev/null +++ b/packages/crypto/src/keys/rsa-class.ts @@ -0,0 +1,156 @@ + +import { CodeError } from '@libp2p/interfaces/errors' +import { sha256 } from 'multiformats/hashes/sha2' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import 'node-forge/lib/sha512.js' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { exporter } from './exporter.js' +import * as pbm from './keys.js' +import * as crypto from './rsa.js' +import type { Multibase } from 'multiformats' + +export class RsaPublicKey { + private readonly _key: JsonWebKey + + constructor (key: JsonWebKey) { + this._key = key + } + + async verify (data: Uint8Array, sig: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal (): Uint8Array { + return crypto.utils.jwkToPkix(this._key) + } + + get bytes (): Uint8Array { + return pbm.PublicKey.encode({ + Type: pbm.KeyType.RSA, + Data: this.marshal() + }).subarray() + } + + encrypt (bytes: Uint8Array): Uint8Array { + return crypto.encrypt(this._key, bytes) + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } +} + +export class RsaPrivateKey { + private readonly _key: JsonWebKey + private readonly _publicKey: JsonWebKey + + constructor (key: JsonWebKey, publicKey: JsonWebKey) { + this._key = key + this._publicKey = publicKey + } + + genSecret (): Uint8Array { + return crypto.getRandomValues(16) + } + + async sign (message: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndSign(this._key, message) + } + + get public (): RsaPublicKey { + if (this._publicKey == null) { + throw new CodeError('public key not provided', 'ERR_PUBKEY_NOT_PROVIDED') + } + + return new RsaPublicKey(this._publicKey) + } + + decrypt (bytes: Uint8Array): Uint8Array { + return crypto.decrypt(this._key, bytes) + } + + marshal (): Uint8Array { + return crypto.utils.jwkToPkcs1(this._key) + } + + get bytes (): Uint8Array { + return pbm.PrivateKey.encode({ + Type: pbm.KeyType.RSA, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + */ + async id (): Promise { + const hash = await this.public.hash() + return uint8ArrayToString(hash, 'base58btc') + } + + /** + * Exports the key into a password protected PEM format + */ + async export (password: string, format = 'pkcs-8'): Promise> { // eslint-disable-line require-await + if (format === 'pkcs-8') { + const buffer = new forge.util.ByteBuffer(this.marshal()) + const asn1 = forge.asn1.fromDer(buffer) + const privateKey = forge.pki.privateKeyFromAsn1(asn1) + + const options = { + algorithm: 'aes256', + count: 10000, + saltSize: 128 / 8, + prfAlgorithm: 'sha512' + } + return forge.pki.encryptRsaPrivateKey(privateKey, password, options) + } else if (format === 'libp2p-key') { + return exporter(this.bytes, password) + } else { + throw new CodeError(`export format '${format}' is not supported`, 'ERR_INVALID_EXPORT_FORMAT') + } + } +} + +export async function unmarshalRsaPrivateKey (bytes: Uint8Array): Promise { + const jwk = crypto.utils.pkcs1ToJwk(bytes) + const keys = await crypto.unmarshalPrivateKey(jwk) + return new RsaPrivateKey(keys.privateKey, keys.publicKey) +} + +export function unmarshalRsaPublicKey (bytes: Uint8Array): RsaPublicKey { + const jwk = crypto.utils.pkixToJwk(bytes) + return new RsaPublicKey(jwk) +} + +export async function fromJwk (jwk: JsonWebKey): Promise { + const keys = await crypto.unmarshalPrivateKey(jwk) + return new RsaPrivateKey(keys.privateKey, keys.publicKey) +} + +export async function generateKeyPair (bits: number): Promise { + const keys = await crypto.generateKey(bits) + return new RsaPrivateKey(keys.privateKey, keys.publicKey) +} diff --git a/packages/crypto/src/keys/rsa-utils.ts b/packages/crypto/src/keys/rsa-utils.ts new file mode 100644 index 0000000000..c818362379 --- /dev/null +++ b/packages/crypto/src/keys/rsa-utils.ts @@ -0,0 +1,74 @@ +import 'node-forge/lib/asn1.js' +import 'node-forge/lib/rsa.js' +import { CodeError } from '@libp2p/interfaces/errors' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { bigIntegerToUintBase64url, base64urlToBigInteger } from './../util.js' + +// Convert a PKCS#1 in ASN1 DER format to a JWK key +export function pkcs1ToJwk (bytes: Uint8Array): JsonWebKey { + const asn1 = forge.asn1.fromDer(uint8ArrayToString(bytes, 'ascii')) + const privateKey = forge.pki.privateKeyFromAsn1(asn1) + + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + return { + kty: 'RSA', + n: bigIntegerToUintBase64url(privateKey.n), + e: bigIntegerToUintBase64url(privateKey.e), + d: bigIntegerToUintBase64url(privateKey.d), + p: bigIntegerToUintBase64url(privateKey.p), + q: bigIntegerToUintBase64url(privateKey.q), + dp: bigIntegerToUintBase64url(privateKey.dP), + dq: bigIntegerToUintBase64url(privateKey.dQ), + qi: bigIntegerToUintBase64url(privateKey.qInv), + alg: 'RS256' + } +} + +// Convert a JWK key into PKCS#1 in ASN1 DER format +export function jwkToPkcs1 (jwk: JsonWebKey): Uint8Array { + if (jwk.n == null || jwk.e == null || jwk.d == null || jwk.p == null || jwk.q == null || jwk.dp == null || jwk.dq == null || jwk.qi == null) { + throw new CodeError('JWK was missing components', 'ERR_INVALID_PARAMETERS') + } + + const asn1 = forge.pki.privateKeyToAsn1({ + n: base64urlToBigInteger(jwk.n), + e: base64urlToBigInteger(jwk.e), + d: base64urlToBigInteger(jwk.d), + p: base64urlToBigInteger(jwk.p), + q: base64urlToBigInteger(jwk.q), + dP: base64urlToBigInteger(jwk.dp), + dQ: base64urlToBigInteger(jwk.dq), + qInv: base64urlToBigInteger(jwk.qi) + }) + + return uint8ArrayFromString(forge.asn1.toDer(asn1).getBytes(), 'ascii') +} + +// Convert a PKCIX in ASN1 DER format to a JWK key +export function pkixToJwk (bytes: Uint8Array): JsonWebKey { + const asn1 = forge.asn1.fromDer(uint8ArrayToString(bytes, 'ascii')) + const publicKey = forge.pki.publicKeyFromAsn1(asn1) + + return { + kty: 'RSA', + n: bigIntegerToUintBase64url(publicKey.n), + e: bigIntegerToUintBase64url(publicKey.e) + } +} + +// Convert a JWK key to PKCIX in ASN1 DER format +export function jwkToPkix (jwk: JsonWebKey): Uint8Array { + if (jwk.n == null || jwk.e == null) { + throw new CodeError('JWK was missing components', 'ERR_INVALID_PARAMETERS') + } + + const asn1 = forge.pki.publicKeyToAsn1({ + n: base64urlToBigInteger(jwk.n), + e: base64urlToBigInteger(jwk.e) + }) + + return uint8ArrayFromString(forge.asn1.toDer(asn1).getBytes(), 'ascii') +} diff --git a/packages/crypto/src/keys/rsa.ts b/packages/crypto/src/keys/rsa.ts new file mode 100644 index 0000000000..619874bcd3 --- /dev/null +++ b/packages/crypto/src/keys/rsa.ts @@ -0,0 +1,69 @@ +import crypto from 'crypto' +import { promisify } from 'util' +import { CodeError } from '@libp2p/interfaces/errors' +import randomBytes from '../random-bytes.js' +import * as utils from './rsa-utils.js' +import type { JWKKeyPair } from './interface.js' + +const keypair = promisify(crypto.generateKeyPair) + +export { utils } + +export async function generateKey (bits: number): Promise { // eslint-disable-line require-await + // @ts-expect-error node types are missing jwk as a format + const key = await keypair('rsa', { + modulusLength: bits, + publicKeyEncoding: { type: 'pkcs1', format: 'jwk' }, + privateKeyEncoding: { type: 'pkcs1', format: 'jwk' } + }) + + return { + // @ts-expect-error node types are missing jwk as a format + privateKey: key.privateKey, + // @ts-expect-error node types are missing jwk as a format + publicKey: key.publicKey + } +} + +// Takes a jwk key +export async function unmarshalPrivateKey (key: JsonWebKey): Promise { // eslint-disable-line require-await + if (key == null) { + throw new CodeError('Missing key parameter', 'ERR_MISSING_KEY') + } + return { + privateKey: key, + publicKey: { + kty: key.kty, + n: key.n, + e: key.e + } + } +} + +export { randomBytes as getRandomValues } + +export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise { + return crypto.createSign('RSA-SHA256') + .update(msg) + // @ts-expect-error node types are missing jwk as a format + .sign({ format: 'jwk', key }) +} + +export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.createVerify('RSA-SHA256') + .update(msg) + // @ts-expect-error node types are missing jwk as a format + .verify({ format: 'jwk', key }, sig) +} + +const padding = crypto.constants.RSA_PKCS1_PADDING + +export function encrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array { + // @ts-expect-error node types are missing jwk as a format + return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes) +} + +export function decrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array { + // @ts-expect-error node types are missing jwk as a format + return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes) +} diff --git a/packages/crypto/src/keys/secp256k1-class.ts b/packages/crypto/src/keys/secp256k1-class.ts new file mode 100644 index 0000000000..8df62b487c --- /dev/null +++ b/packages/crypto/src/keys/secp256k1-class.ts @@ -0,0 +1,119 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { exporter } from './exporter.js' +import * as keysProtobuf from './keys.js' +import * as crypto from './secp256k1.js' +import type { Multibase } from 'multiformats' + +export class Secp256k1PublicKey { + private readonly _key: Uint8Array + + constructor (key: Uint8Array) { + crypto.validatePublicKey(key) + this._key = key + } + + async verify (data: Uint8Array, sig: Uint8Array): Promise { + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal (): Uint8Array { + return crypto.compressPublicKey(this._key) + } + + get bytes (): Uint8Array { + return keysProtobuf.PublicKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } +} + +export class Secp256k1PrivateKey { + private readonly _key: Uint8Array + private readonly _publicKey: Uint8Array + + constructor (key: Uint8Array, publicKey?: Uint8Array) { + this._key = key + this._publicKey = publicKey ?? crypto.computePublicKey(key) + crypto.validatePrivateKey(this._key) + crypto.validatePublicKey(this._publicKey) + } + + async sign (message: Uint8Array): Promise { + return crypto.hashAndSign(this._key, message) + } + + get public (): Secp256k1PublicKey { + return new Secp256k1PublicKey(this._publicKey) + } + + marshal (): Uint8Array { + return this._key + } + + get bytes (): Uint8Array { + return keysProtobuf.PrivateKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + */ + async id (): Promise { + const hash = await this.public.hash() + return uint8ArrayToString(hash, 'base58btc') + } + + /** + * Exports the key into a password protected `format` + */ + async export (password: string, format = 'libp2p-key'): Promise> { + if (format === 'libp2p-key') { + return exporter(this.bytes, password) + } else { + throw new CodeError(`export format '${format}' is not supported`, 'ERR_INVALID_EXPORT_FORMAT') + } + } +} + +export function unmarshalSecp256k1PrivateKey (bytes: Uint8Array): Secp256k1PrivateKey { + return new Secp256k1PrivateKey(bytes) +} + +export function unmarshalSecp256k1PublicKey (bytes: Uint8Array): Secp256k1PublicKey { + return new Secp256k1PublicKey(bytes) +} + +export async function generateKeyPair (): Promise { + const privateKeyBytes = crypto.generateKey() + return new Secp256k1PrivateKey(privateKeyBytes) +} diff --git a/packages/crypto/src/keys/secp256k1.ts b/packages/crypto/src/keys/secp256k1.ts new file mode 100644 index 0000000000..a41207a89b --- /dev/null +++ b/packages/crypto/src/keys/secp256k1.ts @@ -0,0 +1,69 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import * as secp from '@noble/secp256k1' +import { sha256 } from 'multiformats/hashes/sha2' + +const PRIVATE_KEY_BYTE_LENGTH = 32 + +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +export function generateKey (): Uint8Array { + return secp.utils.randomPrivateKey() +} + +/** + * Hash and sign message with private key + */ +export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise { + const { digest } = await sha256.digest(msg) + try { + return await secp.sign(digest, key) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_INPUT') + } +} + +/** + * Hash message and verify signature with public key + */ +export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise { + try { + const { digest } = await sha256.digest(msg) + return secp.verify(sig, digest, key) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_INPUT') + } +} + +export function compressPublicKey (key: Uint8Array): Uint8Array { + const point = secp.Point.fromHex(key).toRawBytes(true) + return point +} + +export function decompressPublicKey (key: Uint8Array): Uint8Array { + const point = secp.Point.fromHex(key).toRawBytes(false) + return point +} + +export function validatePrivateKey (key: Uint8Array): void { + try { + secp.getPublicKey(key, true) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY') + } +} + +export function validatePublicKey (key: Uint8Array): void { + try { + secp.Point.fromHex(key) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_PUBLIC_KEY') + } +} + +export function computePublicKey (privateKey: Uint8Array): Uint8Array { + try { + return secp.getPublicKey(privateKey, true) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY') + } +} diff --git a/packages/crypto/src/pbkdf2.ts b/packages/crypto/src/pbkdf2.ts new file mode 100644 index 0000000000..fe2add7dcc --- /dev/null +++ b/packages/crypto/src/pbkdf2.ts @@ -0,0 +1,39 @@ +import { CodeError } from '@libp2p/interfaces/errors' +// @ts-expect-error types are missing +import forgePbkdf2 from 'node-forge/lib/pbkdf2.js' +// @ts-expect-error types are missing +import forgeUtil from 'node-forge/lib/util.js' + +/** + * Maps an IPFS hash name to its node-forge equivalent. + * + * See https://github.com/multiformats/multihash/blob/master/hashtable.csv + * + * @private + */ +const hashName = { + sha1: 'sha1', + 'sha2-256': 'sha256', + 'sha2-512': 'sha512' +} + +/** + * Computes the Password-Based Key Derivation Function 2. + */ +export default function pbkdf2 (password: string, salt: string, iterations: number, keySize: number, hash: string): string { + if (hash !== 'sha1' && hash !== 'sha2-256' && hash !== 'sha2-512') { + const types = Object.keys(hashName).join(' / ') + throw new CodeError(`Hash '${hash}' is unknown or not supported. Must be ${types}`, 'ERR_UNSUPPORTED_HASH_TYPE') + } + + const hasher = hashName[hash] + const dek = forgePbkdf2( + password, + salt, + iterations, + keySize, + hasher + ) + + return forgeUtil.encode64(dek, null) +} diff --git a/packages/crypto/src/random-bytes.ts b/packages/crypto/src/random-bytes.ts new file mode 100644 index 0000000000..7352dfe071 --- /dev/null +++ b/packages/crypto/src/random-bytes.ts @@ -0,0 +1,9 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { utils } from '@noble/secp256k1' + +export default function randomBytes (length: number): Uint8Array { + if (isNaN(length) || length <= 0) { + throw new CodeError('random bytes length must be a Number bigger than 0', 'ERR_INVALID_LENGTH') + } + return utils.randomBytes(length) +} diff --git a/packages/crypto/src/util.ts b/packages/crypto/src/util.ts new file mode 100644 index 0000000000..e0bab8c5ff --- /dev/null +++ b/packages/crypto/src/util.ts @@ -0,0 +1,42 @@ +import 'node-forge/lib/util.js' +import 'node-forge/lib/jsbn.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +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 { + // Call `.abs()` to convert to unsigned + let buf = Uint8Array.from(num.abs().toByteArray()) // toByteArray converts to big endian + + // toByteArray() gives us back a signed array, which will include a leading 0 + // byte if the most significant bit of the number is 1: + // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-integer + // Our number will always be positive so we should remove the leading padding. + buf = buf[0] === 0 ? buf.subarray(1) : buf + + if (len != null) { + if (buf.length > len) throw new Error('byte array longer than desired length') + buf = uint8ArrayConcat([new Uint8Array(len - buf.length), buf]) + } + + return uint8ArrayToString(buf, 'base64url') +} + +// Convert a base64url encoded string to a BigInteger +export function base64urlToBigInteger (str: string): typeof forge.jsbn.BigInteger { + const buf = base64urlToBuffer(str) + return new forge.jsbn.BigInteger(uint8ArrayToString(buf, 'base16'), 16) +} + +export function base64urlToBuffer (str: string, len?: number): Uint8Array { + let buf = uint8ArrayFromString(str, 'base64urlpad') + + if (len != null) { + if (buf.length > len) throw new Error('byte array longer than desired length') + buf = uint8ArrayConcat([new Uint8Array(len - buf.length), buf]) + } + + return buf +} diff --git a/packages/crypto/src/webcrypto.ts b/packages/crypto/src/webcrypto.ts new file mode 100644 index 0000000000..bbf03ffe98 --- /dev/null +++ b/packages/crypto/src/webcrypto.ts @@ -0,0 +1,24 @@ +/* eslint-env browser */ + +// Check native crypto exists and is enabled (In insecure context `self.crypto` +// exists but `self.crypto.subtle` does not). +export default { + get (win = globalThis) { + const nativeCrypto = win.crypto + + if (nativeCrypto == null || nativeCrypto.subtle == null) { + throw Object.assign( + new Error( + 'Missing Web Crypto API. ' + + 'The most likely cause of this error is that this page is being accessed ' + + 'from an insecure context (i.e. not HTTPS). For more information and ' + + 'possible resolutions see ' + + 'https://github.com/libp2p/js-libp2p-crypto/blob/master/README.md#web-crypto-api' + ), + { code: 'ERR_MISSING_WEB_CRYPTO' } + ) + } + + return nativeCrypto + } +} diff --git a/packages/crypto/stats.md b/packages/crypto/stats.md new file mode 100644 index 0000000000..8c4eff33e6 --- /dev/null +++ b/packages/crypto/stats.md @@ -0,0 +1,153 @@ +# Stats + +## Size + +| | non-minified | minified | +|-------|--------------|----------| +|before | `1.8M` | `949K` | +|after | `606K` | `382K` | + +## Performance + +### RSA + +#### Before + +##### Node `6.6.0` + +``` +generateKeyPair 1024bits x 3.51 ops/sec ±29.45% (22 runs sampled) +generateKeyPair 2048bits x 0.17 ops/sec ±145.40% (5 runs sampled) +generateKeyPair 4096bits x 0.02 ops/sec ±96.53% (5 runs sampled) +sign and verify x 95.98 ops/sec ±1.51% (71 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +generateKeyPair 1024bits x 3.56 ops/sec ±27.16% (23 runs sampled) +generateKeyPair 2048bits x 0.49 ops/sec ±69.32% (8 runs sampled) +generateKeyPair 4096bits x 0.03 ops/sec ±77.11% (5 runs sampled) +sign and verify x 109 ops/sec ±2.00% (53 runs sampled) +``` + +#### After + +##### Node `6.6.0` + +``` +generateKeyPair 1024bits x 42.45 ops/sec ±9.87% (52 runs sampled) +generateKeyPair 2048bits x 7.46 ops/sec ±23.80% (16 runs sampled) +generateKeyPair 4096bits x 1.50 ops/sec ±58.59% (13 runs sampled) +sign and verify x 1,080 ops/sec ±2.23% (74 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +generateKeyPair 1024bits x 5.89 ops/sec ±18.94% (19 runs sampled) +generateKeyPair 2048bits x 1.32 ops/sec ±36.84% (10 runs sampled) +generateKeyPair 4096bits x 0.20 ops/sec ±62.49% (5 runs sampled) +sign and verify x 608 ops/sec ±6.75% (56 runs sampled) +``` + +### Key Stretcher + + +#### Before + +##### Node `6.6.0` + +``` +keyStretcher AES-128 SHA1 x 3,863 ops/sec ±3.80% (70 runs sampled) +keyStretcher AES-128 SHA256 x 3,862 ops/sec ±5.33% (64 runs sampled) +keyStretcher AES-128 SHA512 x 3,369 ops/sec ±1.73% (73 runs sampled) +keyStretcher AES-256 SHA1 x 3,008 ops/sec ±4.81% (67 runs sampled) +keyStretcher AES-256 SHA256 x 2,900 ops/sec ±7.01% (64 runs sampled) +keyStretcher AES-256 SHA512 x 2,553 ops/sec ±4.45% (73 runs sampled) +keyStretcher Blowfish SHA1 x 28,045 ops/sec ±7.32% (61 runs sampled) +keyStretcher Blowfish SHA256 x 18,860 ops/sec ±5.36% (67 runs sampled) +keyStretcher Blowfish SHA512 x 12,142 ops/sec ±12.44% (72 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +keyStretcher AES-128 SHA1 x 4,168 ops/sec ±4.08% (49 runs sampled) +keyStretcher AES-128 SHA256 x 4,239 ops/sec ±6.36% (48 runs sampled) +keyStretcher AES-128 SHA512 x 3,600 ops/sec ±5.15% (51 runs sampled) +keyStretcher AES-256 SHA1 x 3,009 ops/sec ±6.82% (48 runs sampled) +keyStretcher AES-256 SHA256 x 3,086 ops/sec ±9.56% (19 runs sampled) +keyStretcher AES-256 SHA512 x 2,470 ops/sec ±2.22% (54 runs sampled) +keyStretcher Blowfish SHA1 x 7,143 ops/sec ±15.17% (9 runs sampled) +keyStretcher Blowfish SHA256 x 17,846 ops/sec ±4.74% (46 runs sampled) +keyStretcher Blowfish SHA512 x 7,726 ops/sec ±1.81% (50 runs sampled) +``` + +#### After + +##### Node `6.6.0` + +``` +keyStretcher AES-128 SHA1 x 6,680 ops/sec ±3.62% (65 runs sampled) +keyStretcher AES-128 SHA256 x 8,124 ops/sec ±4.37% (66 runs sampled) +keyStretcher AES-128 SHA512 x 11,683 ops/sec ±4.56% (66 runs sampled) +keyStretcher AES-256 SHA1 x 5,531 ops/sec ±4.69% (68 runs sampled) +keyStretcher AES-256 SHA256 x 6,725 ops/sec ±4.87% (66 runs sampled) +keyStretcher AES-256 SHA512 x 9,042 ops/sec ±3.87% (64 runs sampled) +keyStretcher Blowfish SHA1 x 40,757 ops/sec ±5.38% (60 runs sampled) +keyStretcher Blowfish SHA256 x 41,845 ops/sec ±4.89% (64 runs sampled) +keyStretcher Blowfish SHA512 x 42,345 ops/sec ±4.86% (63 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +keyStretcher AES-128 SHA1 x 479 ops/sec ±2.12% (54 runs sampled) +keyStretcher AES-128 SHA256 x 668 ops/sec ±2.02% (53 runs sampled) +keyStretcher AES-128 SHA512 x 1,112 ops/sec ±1.61% (54 runs sampled) +keyStretcher AES-256 SHA1 x 460 ops/sec ±1.37% (54 runs sampled) +keyStretcher AES-256 SHA256 x 596 ops/sec ±1.56% (54 runs sampled) +keyStretcher AES-256 SHA512 x 808 ops/sec ±3.27% (52 runs sampled) +keyStretcher Blowfish SHA1 x 3,015 ops/sec ±3.51% (52 runs sampled) +keyStretcher Blowfish SHA256 x 2,755 ops/sec ±3.82% (53 runs sampled) +keyStretcher Blowfish SHA512 x 2,955 ops/sec ±5.35% (51 runs sampled) +``` + +### Ephemeral Keys + +#### Before + +##### Node `6.6.0` + +``` +ephemeral key with secrect P-256 x 89.93 ops/sec ±39.45% (72 runs sampled) +ephemeral key with secrect P-384 x 110 ops/sec ±1.28% (71 runs sampled) +ephemeral key with secrect P-521 x 112 ops/sec ±1.70% (72 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +ephemeral key with secrect P-256 x 6.27 ops/sec ±15.89% (35 runs sampled) +ephemeral key with secrect P-384 x 6.84 ops/sec ±1.21% (35 runs sampled) +ephemeral key with secrect P-521 x 6.60 ops/sec ±1.84% (34 runs sampled) +``` + +#### After + +##### Node `6.6.0` + +``` +ephemeral key with secrect P-256 x 555 ops/sec ±1.61% (75 runs sampled) +ephemeral key with secrect P-384 x 547 ops/sec ±4.40% (68 runs sampled) +ephemeral key with secrect P-521 x 583 ops/sec ±4.84% (72 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +ephemeral key with secrect P-256 x 796 ops/sec ±2.36% (53 runs sampled) +ephemeral key with secrect P-384 x 788 ops/sec ±2.66% (53 runs sampled) +ephemeral key with secrect P-521 x 808 ops/sec ±1.83% (54 runs sampled) +``` diff --git a/packages/crypto/test/aes/aes.spec.ts b/packages/crypto/test/aes/aes.spec.ts new file mode 100644 index 0000000000..0b41c1d2dd --- /dev/null +++ b/packages/crypto/test/aes/aes.spec.ts @@ -0,0 +1,105 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-disable valid-jsdoc */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import * as crypto from '../../src/index.js' +import fixtures from './../fixtures/aes.js' +import goFixtures from './../fixtures/go-aes.js' +import type { AESCipher } from '../../src/aes/index.js' + +const bytes = [{ + length: 16, + hash: 'AES-128' +}, { + length: 32, + hash: 'AES-256' +}] + +describe('AES-CTR', () => { + bytes.forEach(({ length, hash }) => { + it(`${hash} - encrypt and decrypt`, async () => { + const key = new Uint8Array(length) + key.fill(5) + + const iv = new Uint8Array(16) + iv.fill(1) + + const cipher = await crypto.aes.create(key, iv) + + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + }) + }) + + bytes.forEach(({ length, hash }) => { + it(`${hash} - fixed - encrypt and decrypt`, async () => { + const key = new Uint8Array(length) + key.fill(5) + + const iv = new Uint8Array(16) + iv.fill(1) + + const cipher = await crypto.aes.create(key, iv) + // @ts-expect-error cannot index fixtures like this + const fixture = fixtures[length] + + for (let i = 0; i < fixture.inputs.length; i++) { + const input = fixture.inputs[i] + const output = fixture.outputs[i] + const encrypted = await cipher.encrypt(input) + expect(encrypted).to.have.length(output.length) + expect(encrypted).to.eql(output) + const decrypted = await cipher.decrypt(encrypted) + expect(decrypted).to.eql(input) + } + }) + }) + + bytes.forEach(({ length, hash }) => { + // @ts-expect-error cannot index fixtures like this + if (goFixtures[length] == null) { + return + } + + it(`${hash} - go interop - encrypt and decrypt`, async () => { + const key = new Uint8Array(length) + key.fill(5) + + const iv = new Uint8Array(16) + iv.fill(1) + + const cipher = await crypto.aes.create(key, iv) + // @ts-expect-error cannot index fixtures like this + const fixture = goFixtures[length] + + for (let i = 0; i < fixture.inputs.length; i++) { + const input = fixture.inputs[i] + const output = fixture.outputs[i] + const encrypted = await cipher.encrypt(input) + expect(encrypted).to.have.length(output.length) + expect(encrypted).to.eql(output) + const decrypted = await cipher.decrypt(encrypted) + expect(decrypted).to.eql(input) + } + }) + }) + + it('checks key length', () => { + const key = new Uint8Array(5) + const iv = new Uint8Array(16) + return expect(crypto.aes.create(key, iv)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_LENGTH') + }) +}) + +async function encryptAndDecrypt (cipher: AESCipher): Promise { + const data = new Uint8Array(100) + data.fill(Math.ceil(Math.random() * 100)) + + const encrypted = await cipher.encrypt(data) + const decrypted = await cipher.decrypt(encrypted) + + expect(decrypted).to.be.eql(data) +} diff --git a/packages/crypto/test/crypto.spec.ts b/packages/crypto/test/crypto.spec.ts new file mode 100644 index 0000000000..e6f3b186ce --- /dev/null +++ b/packages/crypto/test/crypto.spec.ts @@ -0,0 +1,155 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import * as crypto from '../src/index.js' +import { RsaPrivateKey, RsaPublicKey } from '../src/keys/rsa-class.js' +import fixtures from './fixtures/go-key-rsa.js' + +describe('libp2p-crypto', function () { + this.timeout(20 * 1000) + let key: RsaPrivateKey + before(async () => { + const generated = await crypto.keys.generateKeyPair('RSA', 512) + + if (!(generated instanceof RsaPrivateKey)) { + throw new Error('Key was incorrect type') + } + + key = generated + }) + + it('marshalPublicKey and unmarshalPublicKey', () => { + const key2 = crypto.keys.unmarshalPublicKey(crypto.keys.marshalPublicKey(key.public)) + + if (!(key2 instanceof RsaPublicKey)) { + throw new Error('Wrong key type unmarshalled') + } + + expect(key2.equals(key.public)).to.be.eql(true) + + expect(() => { + crypto.keys.marshalPublicKey(key.public, 'invalid-key-type') + }).to.throw() + }) + + it('marshalPrivateKey and unmarshalPrivateKey', async () => { + expect(() => { + crypto.keys.marshalPrivateKey(key, 'invalid-key-type') + }).to.throw() + + const key2 = await crypto.keys.unmarshalPrivateKey(crypto.keys.marshalPrivateKey(key)) + + if (!(key2 instanceof RsaPrivateKey)) { + throw new Error('Wrong key type unmarshalled') + } + + expect(key2.equals(key)).to.be.eql(true) + expect(key2.public.equals(key.public)).to.be.eql(true) + }) + + it('generateKeyPair', () => { + // @ts-expect-error key type is invalid + return expect(crypto.keys.generateKeyPair('invalid-key-type', 512)).to.eventually.be.rejected.with.property('code', 'ERR_UNSUPPORTED_KEY_TYPE') + }) + + it('generateKeyPairFromSeed', () => { + const seed = crypto.randomBytes(32) + + // @ts-expect-error key type is invalid + return expect(crypto.keys.generateKeyPairFromSeed('invalid-key-type', seed, 512)).to.eventually.be.rejected.with.property('code', 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE') + }) + + // https://github.com/libp2p/js-libp2p-crypto/issues/314 + function isSafari (): boolean { + return typeof navigator !== 'undefined' && navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome') && navigator.userAgent.includes('Mac') + } + + // marshalled keys seem to be slightly different + // unsure as to if this is just a difference in encoding + // or a bug + describe('go interop', () => { + it('unmarshals private key', async () => { + if (isSafari()) { + // eslint-disable-next-line no-console + console.warn('Skipping test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314') + return + } + + const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key) + const hash = fixtures.private.hash + expect(fixtures.private.key).to.eql(key.bytes) + const digest = await key.hash() + expect(digest).to.eql(hash) + }) + + it('unmarshals public key', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.public.key) + const hash = fixtures.public.hash + expect(crypto.keys.marshalPublicKey(key)).to.eql(fixtures.public.key) + const digest = await key.hash() + expect(digest).to.eql(hash) + }) + + it('unmarshal -> marshal, private key', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key) + const marshalled = crypto.keys.marshalPrivateKey(key) + if (isSafari()) { + // eslint-disable-next-line no-console + console.warn('Running differnt test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314') + const key2 = await crypto.keys.unmarshalPrivateKey(marshalled) + expect(key2.bytes).to.eql(key.bytes) + return + } + expect(marshalled).to.eql(fixtures.private.key) + }) + + it('unmarshal -> marshal, public key', () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.public.key) + const marshalled = crypto.keys.marshalPublicKey(key) + expect(uint8ArrayEquals(fixtures.public.key, marshalled)).to.eql(true) + }) + }) + + describe('pbkdf2', () => { + it('generates a derived password using sha1', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'sha1') + expect(p1).to.exist() + expect(p1).to.be.a('string') + }) + + it('generates a derived password using sha2-512', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'sha2-512') + expect(p1).to.exist() + expect(p1).to.be.a('string') + }) + + it('generates the same derived password with the same options', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 10, 512 / 8, 'sha1') + const p2 = crypto.pbkdf2('password', 'at least 16 character salt', 10, 512 / 8, 'sha1') + const p3 = crypto.pbkdf2('password', 'at least 16 character salt', 11, 512 / 8, 'sha1') + expect(p2).to.equal(p1) + expect(p3).to.not.equal(p2) + }) + + it('throws on invalid hash name', () => { + const fn = (): string => crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'shaX-xxx') + expect(fn).to.throw().with.property('code', 'ERR_UNSUPPORTED_HASH_TYPE') + }) + }) + + describe('randomBytes', () => { + it('throws with invalid number passed', () => { + expect(() => { + crypto.randomBytes(-1) + }).to.throw() + }) + + it('generates different random things', () => { + const buf1 = crypto.randomBytes(10) + expect(buf1.length).to.equal(10) + const buf2 = crypto.randomBytes(10) + expect(buf1).to.not.eql(buf2) + }) + }) +}) diff --git a/packages/crypto/test/fixtures/aes.ts b/packages/crypto/test/fixtures/aes.ts new file mode 100644 index 0000000000..f696dd6330 --- /dev/null +++ b/packages/crypto/test/fixtures/aes.ts @@ -0,0 +1,36 @@ +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +export default { + 16: { + inputs: [ + uint8ArrayFromString('Ly8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLw', 'base64'), + uint8ArrayFromString('GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGA', 'base64'), + uint8ArrayFromString('BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw', 'base64'), + uint8ArrayFromString('GRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQ', 'base64'), + uint8ArrayFromString('MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMA', 'base64') + ], + outputs: [ + uint8ArrayFromString('eWgAlzmJFu/qlzoPZBbkblX4+Q+RgN8ZwK+EqWLL52rgZs70HdUkAhrVXh2G24hJ1LAhX8ZblIuE/LZzdKCSwgBhtQDBlRUz+GEgPlaZ7kMmIjePRsFjax9DWmE3P0XLIelK7Q', 'base64'), + uint8ArrayFromString('bax/s27eT9tXTEbMpm645VoxoPxOOkkzmNoDyAp8mHWJKBd/ODnLH2XjH8bfVmJ4ZFZ0kI5/RK/56BZTFkQ85pIWmcFDBTP979JQH/5nuZF7Y82vnJC/Qx/sK2LF6x8yReRkQA', 'base64'), + uint8ArrayFromString('v11+v7QqdpAcvNO/04KqmYbws1NLFypEnsh7mzmpmIUhclodg1tGaVMtKC9NYGEIKAFu9WqsmJIFcoQAsx8sThNtXMfiJAxKtPHga1MNpxv7ZcFiMVrhxUvVkDTrglz324vRhA', 'base64'), + uint8ArrayFromString('v241vvosvogW0YPIcB89t/dfBPlFk+5KEkfFc43iZlxbgLWsQ20RpTQKNx834f2MmiNoPndnxZh9hoy1qkxLcsO8RMUcL3RSIoDoeg7leqEk1KGkkVbX6d4yj1mDIILEbSTM/g', 'base64'), + uint8ArrayFromString('YY4IJUWtfLRhRi1bxH64h9Voq1nnPysqB/VLpc22GDKq2YBwctfRkYfrs9QFUY7HNd0n76cV7aiR+fpsAvdZSeTj/5t5nc1gKyBw0a1gjyvcjBrNIiI1nSmnfevzVQ0OXW3pug', 'base64') + ] + }, + 32: { + inputs: [ + uint8ArrayFromString('RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA', 'base64'), + uint8ArrayFromString('CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg', 'base64'), + uint8ArrayFromString('S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSw', 'base64'), + uint8ArrayFromString('IyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIw', 'base64'), + uint8ArrayFromString('SEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISA', 'base64') + ], + outputs: [ + uint8ArrayFromString('xjtba97WH+ZwAceuEeDC4YpBOqAf+WYQSi5VHuj5eGwmpWkbDGJibIKjEIl0aJm8Bfuj9AA6Ac8ZTrzSzd0whEjRC0MOKtpHlPx+tzwgXSR9Z791UvG+sAGBdnCLmbI4PVs0xg', 'base64'), + uint8ArrayFromString('zvFjePijOR7YVv/AWcGwEU4+UJW96xudr/gHE95Ab8fMoxoQX91GIO8EOqL97QgzXgOlut/SdGXkUmdMSiwzdb2MhOa88xielV2T4nHDHxgJExuEtJgaQX2QVqcpkJ7MTC61bg', 'base64'), + uint8ArrayFromString('maHJQlcu8leI7pfGgXo+zY78bbvutz9f8GHc0SWQ7VT7lZjeWcjQd9UmQRMC/MF9Xky2xvN5/RAt/lIvks4paf7t7I121sXkO30tyD0YbhrrXK//VXc5oJFrzhw+CqZZBxT28w', 'base64'), + uint8ArrayFromString('T9cWHYSC1vjtfOo2104A0/beNls1AjEoAMq8Gbh5pOu9YQ4AU6ZYPjcxT5mIwYXOrGPPSfbYwGsyzqfyGbQ/uMk9WvLfwA2MH/BwnfpajgMoDGo/SSpPUhQpu60XVTv91L9tLg', 'base64'), + uint8ArrayFromString('yeA4QKfgfDETM9Di2DIMSQ//nGxis5BuIZcrQOOZeCcVlyk99RQfF23VbTcjKHptKQogsBm4W7Cxhor8oAJsK97vrgKRSiKD7dbrZhrMfEBlhrotNx00N6tfrFbyZY2Z3qGAUw', 'base64') + ] + } +} diff --git a/packages/crypto/test/fixtures/go-aes.ts b/packages/crypto/test/fixtures/go-aes.ts new file mode 100644 index 0000000000..ca653e97c1 --- /dev/null +++ b/packages/crypto/test/fixtures/go-aes.ts @@ -0,0 +1,19 @@ + +export default { + 16: { + inputs: [ + Uint8Array.from([47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47]), + Uint8Array.from([24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]), + Uint8Array.from([7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]), + Uint8Array.from([25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25]), + Uint8Array.from([48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48]) + ], + outputs: [ + Uint8Array.from([121, 104, 0, 151, 57, 137, 22, 239, 234, 151, 58, 15, 100, 22, 228, 110, 85, 248, 249, 15, 145, 128, 223, 25, 192, 175, 132, 169, 98, 203, 231, 106, 224, 102, 206, 244, 29, 213, 36, 2, 26, 213, 94, 29, 134, 219, 136, 73, 212, 176, 33, 95, 198, 91, 148, 139, 132, 252, 182, 115, 116, 160, 146, 194, 0, 97, 181, 0, 193, 149, 21, 51, 248, 97, 32, 62, 86, 153, 238, 67, 38, 34, 55, 143, 70, 193, 99, 107, 31, 67, 90, 97, 55, 63, 69, 203, 33, 233, 74, 237]), + Uint8Array.from([109, 172, 127, 179, 110, 222, 79, 219, 87, 76, 70, 204, 166, 110, 184, 229, 90, 49, 160, 252, 78, 58, 73, 51, 152, 218, 3, 200, 10, 124, 152, 117, 137, 40, 23, 127, 56, 57, 203, 31, 101, 227, 31, 198, 223, 86, 98, 120, 100, 86, 116, 144, 142, 127, 68, 175, 249, 232, 22, 83, 22, 68, 60, 230, 146, 22, 153, 193, 67, 5, 51, 253, 239, 210, 80, 31, 254, 103, 185, 145, 123, 99, 205, 175, 156, 144, 191, 67, 31, 236, 43, 98, 197, 235, 31, 50, 69, 228, 100, 64]), + Uint8Array.from([191, 93, 126, 191, 180, 42, 118, 144, 28, 188, 211, 191, 211, 130, 170, 153, 134, 240, 179, 83, 75, 23, 42, 68, 158, 200, 123, 155, 57, 169, 152, 133, 33, 114, 90, 29, 131, 91, 70, 105, 83, 45, 40, 47, 77, 96, 97, 8, 40, 1, 110, 245, 106, 172, 152, 146, 5, 114, 132, 0, 179, 31, 44, 78, 19, 109, 92, 199, 226, 36, 12, 74, 180, 241, 224, 107, 83, 13, 167, 27, 251, 101, 193, 98, 49, 90, 225, 197, 75, 213, 144, 52, 235, 130, 92, 247, 219, 139, 209, 132]), + Uint8Array.from([191, 110, 53, 190, 250, 44, 190, 136, 22, 209, 131, 200, 112, 31, 61, 183, 247, 95, 4, 249, 69, 147, 238, 74, 18, 71, 197, 115, 141, 226, 102, 92, 91, 128, 181, 172, 67, 109, 17, 165, 52, 10, 55, 31, 55, 225, 253, 140, 154, 35, 104, 62, 119, 103, 197, 152, 125, 134, 140, 181, 170, 76, 75, 114, 195, 188, 68, 197, 28, 47, 116, 82, 34, 128, 232, 122, 14, 229, 122, 161, 36, 212, 161, 164, 145, 86, 215, 233, 222, 50, 143, 89, 131, 32, 130, 196, 109, 36, 204, 254]), + Uint8Array.from([97, 142, 8, 37, 69, 173, 124, 180, 97, 70, 45, 91, 196, 126, 184, 135, 213, 104, 171, 89, 231, 63, 43, 42, 7, 245, 75, 165, 205, 182, 24, 50, 170, 217, 128, 112, 114, 215, 209, 145, 135, 235, 179, 212, 5, 81, 142, 199, 53, 221, 39, 239, 167, 21, 237, 168, 145, 249, 250, 108, 2, 247, 89, 73, 228, 227, 255, 155, 121, 157, 205, 96, 43, 32, 112, 209, 173, 96, 143, 43, 220, 140, 26, 205, 34, 34, 53, 157, 41, 167, 125, 235, 243, 85, 13, 14, 93, 109, 233, 186]) + ] + } +} diff --git a/packages/crypto/test/fixtures/go-elliptic-key.ts b/packages/crypto/test/fixtures/go-elliptic-key.ts new file mode 100644 index 0000000000..d5435775b8 --- /dev/null +++ b/packages/crypto/test/fixtures/go-elliptic-key.ts @@ -0,0 +1,12 @@ + +export default { + curve: 'P-256', + bob: { + private: Uint8Array.from([ + 181, 217, 162, 151, 225, 36, 53, 253, 107, 66, 27, 27, 232, 72, 0, 0, 103, 167, 84, 62, 203, 91, 97, 137, 131, 193, 230, 126, 98, 242, 216, 170 + ]), + public: Uint8Array.from([ + 4, 53, 59, 128, 56, 162, 250, 72, 141, 206, 117, 232, 57, 96, 39, 39, 247, 7, 27, 57, 251, 232, 120, 186, 21, 239, 176, 139, 195, 129, 125, 85, 11, 188, 191, 32, 227, 0, 6, 163, 101, 68, 208, 1, 43, 131, 124, 112, 102, 91, 104, 79, 16, 119, 152, 208, 4, 147, 155, 83, 20, 146, 104, 55, 90 + ]) + } +} diff --git a/packages/crypto/test/fixtures/go-key-ed25519.ts b/packages/crypto/test/fixtures/go-key-ed25519.ts new file mode 100644 index 0000000000..e758d788e1 --- /dev/null +++ b/packages/crypto/test/fixtures/go-key-ed25519.ts @@ -0,0 +1,41 @@ + +export default { + // Generation code from https://github.com/libp2p/js-libp2p-crypto/issues/175#issuecomment-634467463 + // + // package main + // + // import ( + // "crypto/rand" + // "fmt" + // "strings" + + // "github.com/libp2p/go-libp2p-core/crypto" + // ) + + // func main() { + // priv, pub, _ := crypto.GenerateEd25519Key(rand.Reader) + // pubkeyBytes, _ := pub.Bytes() + // privkeyBytes, _ := priv.Bytes() + // data := []byte("hello! and welcome to some awesome crypto primitives") + // sig, _ := priv.Sign(data) + // fmt.Println("{\n publicKey: Uint8Array.from(", strings.Replace(fmt.Sprint(pubkeyBytes), " ", ",", -1), "),") + // fmt.Println(" privateKey: Uint8Array.from(", strings.Replace(fmt.Sprint(privkeyBytes), " ", ",", -1), "),") + // fmt.Println(" data: Uint8Array.from(", strings.Replace(fmt.Sprint(data), " ", ",", -1), "),") + // fmt.Println(" signature: Uint8Array.from(", strings.Replace(fmt.Sprint(sig), " ", ",", -1), ")\n}") + // } + // + + // The legacy key unnecessarily appends the publickey. (It's already included) See https://github.com/libp2p/js-libp2p-crypto/issues/175 + redundantPubKey: { + privateKey: Uint8Array.from([8, 1, 18, 96, 201, 208, 1, 110, 176, 16, 230, 37, 66, 184, 149, 252, 78, 56, 206, 136, 2, 38, 118, 152, 226, 197, 117, 200, 54, 189, 156, 218, 184, 7, 118, 57, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]), + publicKey: Uint8Array.from([8, 1, 18, 32, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]), + data: Uint8Array.from([104, 101, 108, 108, 111, 33, 32, 97, 110, 100, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 115, 111, 109, 101, 32, 97, 119, 101, 115, 111, 109, 101, 32, 99, 114, 121, 112, 116, 111, 32, 112, 114, 105, 109, 105, 116, 105, 118, 101, 115]), + signature: Uint8Array.from([7, 230, 175, 164, 228, 58, 78, 208, 62, 243, 73, 142, 83, 195, 176, 217, 166, 62, 41, 165, 168, 164, 75, 179, 163, 86, 102, 32, 18, 84, 150, 237, 39, 207, 213, 20, 134, 237, 50, 41, 176, 183, 229, 133, 38, 255, 42, 228, 68, 186, 100, 14, 175, 156, 243, 118, 125, 125, 120, 212, 124, 103, 252, 12]) + }, + verify: { + publicKey: Uint8Array.from([8, 1, 18, 32, 163, 176, 195, 47, 254, 208, 49, 5, 192, 102, 32, 63, 58, 202, 171, 153, 146, 164, 25, 212, 25, 91, 146, 26, 117, 165, 148, 6, 207, 90, 217, 126]), + privateKey: Uint8Array.from([8, 1, 18, 64, 232, 56, 175, 20, 240, 160, 19, 47, 92, 88, 115, 221, 164, 13, 36, 162, 158, 136, 247, 31, 29, 231, 76, 143, 12, 91, 193, 4, 88, 33, 67, 23, 163, 176, 195, 47, 254, 208, 49, 5, 192, 102, 32, 63, 58, 202, 171, 153, 146, 164, 25, 212, 25, 91, 146, 26, 117, 165, 148, 6, 207, 90, 217, 126]), + data: Uint8Array.from([104, 101, 108, 108, 111, 33, 32, 97, 110, 100, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 115, 111, 109, 101, 32, 97, 119, 101, 115, 111, 109, 101, 32, 99, 114, 121, 112, 116, 111, 32, 112, 114, 105, 109, 105, 116, 105, 118, 101, 115]), + signature: Uint8Array.from([160, 125, 30, 62, 213, 189, 239, 92, 87, 76, 205, 169, 251, 149, 187, 57, 96, 85, 175, 213, 22, 132, 229, 60, 196, 18, 117, 194, 12, 174, 135, 31, 39, 168, 174, 103, 78, 55, 37, 222, 37, 172, 222, 239, 153, 63, 197, 152, 67, 167, 191, 215, 161, 212, 216, 163, 81, 77, 45, 228, 151, 79, 101, 1]) + } +} diff --git a/packages/crypto/test/fixtures/go-key-rsa.ts b/packages/crypto/test/fixtures/go-key-rsa.ts new file mode 100644 index 0000000000..bfdfd1b3ca --- /dev/null +++ b/packages/crypto/test/fixtures/go-key-rsa.ts @@ -0,0 +1,30 @@ + +export default { + private: { + hash: Uint8Array.from([ + 18, 32, 168, 125, 165, 65, 34, 157, 209, 4, 24, 158, 80, 196, 125, 86, 103, 0, 228, 145, 109, 252, 153, 7, 189, 9, 16, 37, 239, 36, 48, 78, 214, 212 + ]), + key: Uint8Array.from([ + 8, 0, 18, 192, 2, 48, 130, 1, 60, 2, 1, 0, 2, 65, 0, 230, 157, 160, 242, 74, 222, 87, 0, 77, 180, 91, 175, 217, 166, 2, 95, 193, 239, 195, 140, 224, 57, 84, 207, 46, 172, 113, 196, 20, 133, 117, 205, 45, 7, 224, 41, 40, 195, 254, 124, 14, 84, 223, 147, 67, 198, 48, 36, 53, 161, 112, 46, 153, 90, 19, 123, 94, 247, 5, 116, 1, 238, 32, 15, 2, 3, 1, 0, 1, 2, 65, 0, 191, 59, 140, 255, 254, 23, 123, 91, 148, 19, 240, 71, 213, 26, 181, 51, 68, 181, 150, 153, 214, 65, 148, 83, 45, 103, 239, 250, 225, 237, 125, 173, 111, 244, 37, 124, 87, 178, 86, 10, 14, 207, 63, 105, 213, 37, 81, 23, 230, 4, 222, 179, 144, 40, 252, 163, 190, 7, 241, 221, 28, 54, 225, 209, 2, 33, 0, 235, 132, 229, 150, 99, 182, 176, 194, 198, 65, 210, 160, 184, 70, 82, 49, 235, 199, 14, 11, 92, 66, 237, 45, 220, 72, 235, 1, 244, 145, 205, 57, 2, 33, 0, 250, 171, 146, 180, 188, 194, 14, 152, 52, 64, 38, 52, 158, 86, 46, 109, 66, 100, 122, 43, 88, 167, 143, 98, 104, 143, 160, 60, 171, 185, 31, 135, 2, 33, 0, 206, 47, 255, 203, 100, 170, 137, 31, 75, 240, 78, 84, 212, 95, 4, 16, 158, 73, 27, 27, 136, 255, 50, 163, 166, 169, 211, 204, 87, 111, 217, 201, 2, 33, 0, 177, 51, 194, 213, 3, 175, 7, 84, 47, 115, 189, 206, 106, 180, 47, 195, 203, 48, 110, 112, 224, 14, 43, 189, 124, 127, 51, 222, 79, 226, 225, 87, 2, 32, 67, 23, 190, 222, 106, 22, 115, 139, 217, 244, 178, 53, 153, 99, 5, 176, 72, 77, 193, 61, 67, 134, 37, 238, 69, 66, 159, 28, 39, 5, 238, 125 + ]) + }, + public: { + hash: Uint8Array.from([ + 18, 32, 112, 151, 163, 167, 204, 243, 175, 123, 208, 162, 90, 84, 199, 174, 202, 110, 0, 119, 27, 202, 7, 149, 161, 251, 215, 168, 163, 54, 93, 54, 195, 20 + ]), + key: Uint8Array.from([ + 8, 0, 18, 94, 48, 92, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 75, 0, 48, 72, 2, 65, 0, 230, 157, 160, 242, 74, 222, 87, 0, 77, 180, 91, 175, 217, 166, 2, 95, 193, 239, 195, 140, 224, 57, 84, 207, 46, 172, 113, 196, 20, 133, 117, 205, 45, 7, 224, 41, 40, 195, 254, 124, 14, 84, 223, 147, 67, 198, 48, 36, 53, 161, 112, 46, 153, 90, 19, 123, 94, 247, 5, 116, 1, 238, 32, 15, 2, 3, 1, 0, 1 + ]) + }, + verify: { + signature: Uint8Array.from([ + 3, 116, 81, 57, 91, 194, 7, 1, 230, 236, 229, 142, 36, 209, 208, 107, 47, 52, 164, 236, 139, 35, 155, 97, 43, 64, 145, 91, 19, 218, 149, 63, 99, 164, 191, 110, 145, 37, 18, 7, 98, 112, 144, 35, 29, 186, 169, 150, 165, 88, 145, 170, 197, 110, 42, 163, 188, 10, 42, 63, 34, 93, 91, 94, 199, 110, 10, 82, 238, 80, 93, 93, 77, 130, 22, 216, 229, 172, 36, 229, 82, 162, 20, 78, 19, 46, 82, 243, 43, 80, 115, 125, 145, 231, 194, 224, 30, 187, 55, 228, 74, 52, 203, 191, 254, 148, 136, 218, 62, 147, 171, 130, 251, 181, 105, 29, 238, 207, 197, 249, 61, 105, 202, 172, 160, 174, 43, 124, 115, 130, 169, 30, 76, 41, 52, 200, 2, 26, 53, 190, 43, 20, 203, 10, 217, 250, 47, 102, 92, 103, 197, 22, 108, 184, 74, 218, 82, 202, 180, 98, 13, 114, 12, 92, 1, 139, 150, 170, 8, 92, 32, 116, 168, 219, 157, 162, 28, 77, 29, 29, 74, 136, 144, 49, 173, 245, 253, 76, 167, 200, 169, 163, 7, 49, 133, 120, 99, 191, 53, 10, 66, 26, 234, 240, 139, 235, 134, 30, 55, 248, 150, 100, 242, 150, 159, 198, 44, 78, 150, 7, 133, 139, 59, 76, 3, 225, 94, 13, 89, 122, 34, 95, 95, 107, 74, 169, 171, 169, 222, 25, 191, 182, 148, 116, 66, 67, 102, 12, 193, 217, 247, 243, 148, 233, 161, 157 + ]), + data: Uint8Array.from([ + 10, 16, 27, 128, 228, 220, 147, 176, 53, 105, 175, 171, 32, 213, 35, 236, 203, 60, 18, 171, 2, 8, 0, 18, 166, 2, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 181, 113, 138, 108, 208, 103, 166, 102, 37, 36, 204, 250, 228, 165, 44, 64, 176, 210, 205, 141, 241, 55, 200, 110, 98, 68, 85, 199, 254, 19, 86, 204, 63, 250, 167, 38, 59, 249, 146, 228, 73, 171, 63, 18, 96, 104, 191, 137, 186, 244, 255, 90, 16, 119, 195, 52, 177, 213, 254, 187, 174, 84, 174, 173, 12, 236, 53, 234, 3, 209, 82, 37, 78, 111, 214, 135, 76, 195, 9, 242, 134, 188, 153, 84, 139, 231, 51, 146, 177, 60, 12, 25, 158, 91, 215, 152, 7, 0, 84, 35, 36, 230, 227, 67, 198, 72, 50, 110, 37, 209, 98, 193, 65, 93, 173, 199, 4, 198, 102, 99, 148, 144, 224, 217, 114, 53, 144, 245, 251, 114, 211, 20, 82, 163, 123, 75, 16, 192, 106, 213, 128, 2, 11, 200, 203, 84, 41, 199, 224, 155, 171, 217, 64, 109, 116, 188, 151, 183, 173, 52, 205, 164, 93, 13, 251, 65, 182, 160, 154, 185, 239, 33, 184, 84, 159, 105, 101, 173, 194, 251, 123, 84, 92, 66, 61, 180, 45, 104, 162, 224, 214, 233, 64, 220, 165, 2, 104, 116, 150, 2, 234, 203, 112, 21, 124, 23, 48, 66, 30, 63, 30, 36, 246, 135, 203, 218, 115, 22, 189, 39, 39, 125, 205, 65, 222, 220, 77, 18, 84, 121, 161, 153, 125, 25, 139, 137, 170, 239, 150, 106, 119, 168, 216, 140, 113, 121, 26, 53, 118, 110, 53, 192, 244, 252, 145, 85, 2, 3, 1, 0, 1, 26, 17, 80, 45, 50, 53, 54, 44, 80, 45, 51, 56, 52, 44, 80, 45, 53, 50, 49, 34, 24, 65, 69, 83, 45, 50, 53, 54, 44, 65, 69, 83, 45, 49, 50, 56, 44, 66, 108, 111, 119, 102, 105, 115, 104, 42, 13, 83, 72, 65, 50, 53, 54, 44, 83, 72, 65, 53, 49, 50, 10, 16, 220, 83, 240, 105, 6, 203, 78, 83, 210, 115, 6, 106, 98, 82, 1, 161, 18, 171, 2, 8, 0, 18, 166, 2, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 185, 234, 19, 191, 164, 33, 65, 94, 87, 42, 74, 83, 224, 25, 142, 44, 26, 7, 92, 242, 189, 42, 170, 197, 178, 92, 45, 240, 107, 141, 128, 59, 122, 252, 48, 140, 4, 85, 85, 203, 3, 197, 8, 127, 120, 98, 44, 169, 135, 196, 70, 137, 117, 180, 177, 134, 170, 35, 165, 88, 105, 30, 114, 138, 11, 96, 68, 99, 18, 149, 223, 166, 105, 12, 176, 77, 48, 214, 22, 236, 17, 154, 213, 209, 158, 169, 202, 5, 100, 210, 83, 90, 201, 38, 205, 246, 231, 106, 63, 86, 222, 143, 157, 173, 62, 4, 85, 232, 20, 188, 6, 209, 186, 132, 192, 117, 146, 181, 233, 26, 0, 240, 138, 206, 91, 170, 114, 137, 217, 132, 139, 242, 144, 213, 103, 101, 190, 146, 188, 250, 188, 134, 255, 70, 125, 78, 65, 136, 239, 190, 206, 139, 155, 140, 163, 233, 170, 247, 205, 87, 209, 19, 29, 173, 10, 147, 43, 28, 90, 46, 6, 197, 217, 186, 66, 68, 126, 76, 64, 184, 8, 170, 23, 79, 243, 223, 119, 133, 118, 50, 226, 44, 246, 176, 10, 161, 219, 83, 54, 68, 248, 5, 14, 177, 114, 54, 63, 11, 71, 136, 142, 56, 151, 123, 230, 61, 80, 15, 180, 42, 49, 220, 148, 99, 231, 20, 230, 220, 85, 207, 187, 37, 210, 137, 171, 125, 71, 14, 53, 100, 91, 83, 209, 50, 132, 165, 253, 25, 161, 5, 97, 164, 163, 83, 95, 53, 2, 3, 1, 0, 1, 26, 17, 80, 45, 50, 53, 54, 44, 80, 45, 51, 56, 52, 44, 80, 45, 53, 50, 49, 34, 15, 65, 69, 83, 45, 50, 53, 54, 44, 65, 69, 83, 45, 49, 50, 56, 42, 13, 83, 72, 65, 50, 53, 54, 44, 83, 72, 65, 53, 49, 50, 4, 97, 54, 203, 112, 136, 34, 231, 162, 19, 154, 131, 27, 105, 26, 121, 238, 120, 25, 203, 66, 232, 53, 198, 20, 19, 96, 119, 218, 90, 64, 170, 3, 132, 116, 1, 87, 116, 232, 165, 161, 198, 117, 167, 60, 145, 1, 253, 108, 50, 150, 117, 8, 140, 133, 48, 30, 236, 36, 84, 186, 22, 144, 87, 101 + ]), + publicKey: Uint8Array.from([ + 8, 0, 18, 166, 2, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 181, 113, 138, 108, 208, 103, 166, 102, 37, 36, 204, 250, 228, 165, 44, 64, 176, 210, 205, 141, 241, 55, 200, 110, 98, 68, 85, 199, 254, 19, 86, 204, 63, 250, 167, 38, 59, 249, 146, 228, 73, 171, 63, 18, 96, 104, 191, 137, 186, 244, 255, 90, 16, 119, 195, 52, 177, 213, 254, 187, 174, 84, 174, 173, 12, 236, 53, 234, 3, 209, 82, 37, 78, 111, 214, 135, 76, 195, 9, 242, 134, 188, 153, 84, 139, 231, 51, 146, 177, 60, 12, 25, 158, 91, 215, 152, 7, 0, 84, 35, 36, 230, 227, 67, 198, 72, 50, 110, 37, 209, 98, 193, 65, 93, 173, 199, 4, 198, 102, 99, 148, 144, 224, 217, 114, 53, 144, 245, 251, 114, 211, 20, 82, 163, 123, 75, 16, 192, 106, 213, 128, 2, 11, 200, 203, 84, 41, 199, 224, 155, 171, 217, 64, 109, 116, 188, 151, 183, 173, 52, 205, 164, 93, 13, 251, 65, 182, 160, 154, 185, 239, 33, 184, 84, 159, 105, 101, 173, 194, 251, 123, 84, 92, 66, 61, 180, 45, 104, 162, 224, 214, 233, 64, 220, 165, 2, 104, 116, 150, 2, 234, 203, 112, 21, 124, 23, 48, 66, 30, 63, 30, 36, 246, 135, 203, 218, 115, 22, 189, 39, 39, 125, 205, 65, 222, 220, 77, 18, 84, 121, 161, 153, 125, 25, 139, 137, 170, 239, 150, 106, 119, 168, 216, 140, 113, 121, 26, 53, 118, 110, 53, 192, 244, 252, 145, 85, 2, 3, 1, 0, 1 + ]) + } +} diff --git a/packages/crypto/test/fixtures/go-key-secp256k1.ts b/packages/crypto/test/fixtures/go-key-secp256k1.ts new file mode 100644 index 0000000000..789d52aa2a --- /dev/null +++ b/packages/crypto/test/fixtures/go-key-secp256k1.ts @@ -0,0 +1,29 @@ +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +// The keypair and signature below were generated in a gore repl session (https://github.com/motemen/gore) +// using the secp256k1 fork of go-libp2p-crypto by github user @vyzo +// +// gore> :import github.com/vyzo/go-libp2p-crypto +// gore> :import crypto/rand +// gore> :import io/ioutil +// gore> priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Secp256k1, 256, rand.Reader) +// gore> privBytes, err := priv.Bytes() +// gore> pubBytes, err := pub.Bytes() +// gore> msg := []byte("hello! and welcome to some awesome crypto primitives") +// gore> sig, err := priv.Sign(msg) +// gore> ioutil.WriteFile("/tmp/secp-go-priv.bin", privBytes, 0644) +// gore> ioutil.WriteFile("/tmp/secp-go-pub.bin", pubBytes, 0644) +// gore> ioutil.WriteFile("/tmp/secp-go-sig.bin", sig, 0644) +// +// The generated files were then read in a node repl with e.g.: +// > fs.readFileSync('/tmp/secp-go-pub.bin').toString('hex') +// '08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf' +// +// and the results copy/pasted in here + +export default { + privateKey: uint8ArrayFromString('08021220358f15db8c2014d570e8e3a622454e2273975a3cca443ec0c45375b13d381d18', 'base16'), + publicKey: uint8ArrayFromString('08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf', 'base16'), + message: uint8ArrayFromString('hello! and welcome to some awesome crypto primitives'), + signature: uint8ArrayFromString('304402200e4c629e9f5d99439115e60989cd40087f6978c36078b0b50cf3d30af5c38d4102204110342c8e7f0809897c1c7a66e49e1c6b7cb0a6ed6993640ec2fe742c1899a9', 'base16') +} diff --git a/packages/crypto/test/fixtures/go-stretch-key.ts b/packages/crypto/test/fixtures/go-stretch-key.ts new file mode 100644 index 0000000000..4fd4e9c4c0 --- /dev/null +++ b/packages/crypto/test/fixtures/go-stretch-key.ts @@ -0,0 +1,30 @@ + +export default [{ + cipher: 'AES-256' as 'AES-256', + hash: 'SHA256' as 'SHA256', + secret: Uint8Array.from([ + 195, 191, 209, 165, 209, 201, 127, 122, 136, 111, 31, 66, 111, 68, 38, 155, 216, 204, 46, 181, 200, 188, 170, 204, 104, 74, 239, 251, 173, 114, 222, 234 + ]), + k1: { + iv: Uint8Array.from([ + 208, 132, 203, 169, 253, 52, 40, 83, 161, 91, 17, 71, 33, 136, 67, 96 + ]), + cipherKey: Uint8Array.from([ + 156, 48, 241, 157, 92, 248, 153, 186, 114, 127, 195, 114, 106, 104, 215, 133, 35, 11, 131, 137, 123, 70, 74, 26, 15, 60, 189, 32, 67, 221, 115, 137 + ]), + macKey: Uint8Array.from([ + 6, 179, 91, 245, 224, 56, 153, 120, 77, 140, 29, 5, 15, 213, 187, 65, 137, 230, 202, 120 + ]) + }, + k2: { + iv: Uint8Array.from([ + 236, 17, 34, 141, 90, 106, 197, 56, 197, 184, 157, 135, 91, 88, 112, 19 + ]), + cipherKey: Uint8Array.from([ + 151, 145, 195, 219, 76, 195, 102, 109, 187, 231, 100, 150, 132, 245, 251, 130, 254, 37, 178, 55, 227, 34, 114, 39, 238, 34, 2, 193, 107, 130, 32, 87 + ]), + macKey: Uint8Array.from([ + 3, 229, 77, 212, 241, 217, 23, 113, 220, 126, 38, 255, 18, 117, 108, 205, 198, 89, 1, 236 + ]) + } +}] diff --git a/packages/crypto/test/fixtures/secp256k1.ts b/packages/crypto/test/fixtures/secp256k1.ts new file mode 100644 index 0000000000..507716f0db --- /dev/null +++ b/packages/crypto/test/fixtures/secp256k1.ts @@ -0,0 +1,8 @@ +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +export default { + // protobuf marshalled key pair generated with libp2p-crypto-secp256k1 + // and marshalled with libp2p-crypto.marshalPublicKey / marshalPrivateKey + pbmPrivateKey: uint8ArrayFromString('08021220e0600103010000000100000000000000be1dc82c2e000000e8d6030301000000', 'base16'), + pbmPublicKey: uint8ArrayFromString('0802122103a9a7272a726fa083abf31ba44037f8347fbc5e5d3113d62a7c6bc26752fd8ee1', 'base16') +} diff --git a/packages/crypto/test/helpers/test-garbage-error-handling.ts b/packages/crypto/test/helpers/test-garbage-error-handling.ts new file mode 100644 index 0000000000..60316b26f2 --- /dev/null +++ b/packages/crypto/test/helpers/test-garbage-error-handling.ts @@ -0,0 +1,28 @@ +/* eslint-env mocha */ +import util from 'util' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +const garbage = [uint8ArrayFromString('00010203040506070809', 'base16'), {}, null, false, undefined, true, 1, 0, uint8ArrayFromString(''), 'aGVsbG93b3JsZA==', 'helloworld', ''] + +export function testGarbage (fncName: string, fnc: (...args: Uint8Array[]) => Promise, num?: number, skipBuffersAndStrings?: boolean): void { + const count = num ?? 1 + + garbage.forEach((garbage) => { + if (skipBuffersAndStrings === true && (garbage instanceof Uint8Array || (typeof garbage) === 'string')) { + // skip this garbage because it's a Uint8Array or a String and we were told do do that + return + } + const args: any[] = [] + for (let i = 0; i < count; i++) { + args.push(garbage) + } + it(fncName + '(' + args.map(garbage => util.inspect(garbage)).join(', ') + ')', async () => { + try { + await fnc.apply(null, args) + } catch (err) { + return // expected + } + throw new Error('Expected error to be thrown') + }) + }) +} diff --git a/packages/crypto/test/hmac/hmac.spec.ts b/packages/crypto/test/hmac/hmac.spec.ts new file mode 100644 index 0000000000..8ead33bfae --- /dev/null +++ b/packages/crypto/test/hmac/hmac.spec.ts @@ -0,0 +1,17 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' + +const hashes = ['SHA1', 'SHA256', 'SHA512'] as ['SHA1', 'SHA256', 'SHA512'] + +describe('HMAC', () => { + hashes.forEach((hash) => { + it(`${hash} - sign and verify`, async () => { + const hmac = await crypto.hmac.create(hash, uint8ArrayFromString('secret')) + const sig = await hmac.digest(uint8ArrayFromString('hello world')) + expect(sig).to.have.length(hmac.length) + }) + }) +}) diff --git a/packages/crypto/test/keys/ed25519.spec.ts b/packages/crypto/test/keys/ed25519.spec.ts new file mode 100644 index 0000000000..a52706b067 --- /dev/null +++ b/packages/crypto/test/keys/ed25519.spec.ts @@ -0,0 +1,224 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' +import { Ed25519PrivateKey } from '../../src/keys/ed25519-class.js' +import fixtures from '../fixtures/go-key-ed25519.js' +import { testGarbage } from '../helpers/test-garbage-error-handling.js' + +const ed25519 = crypto.keys.supportedKeys.ed25519 + +/** @typedef {import("libp2p-crypto").PrivateKey} PrivateKey */ + +describe('ed25519', function () { + this.timeout(20 * 1000) + let key: Ed25519PrivateKey + before(async () => { + const generated = await crypto.keys.generateKeyPair('Ed25519', 512) + + if (!(generated instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + key = generated + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(ed25519.Ed25519PrivateKey) + const digest = await key.hash() + expect(digest).to.have.length(34) + }) + + it('generates a valid key from seed', async () => { + const seed = crypto.randomBytes(32) + const seededkey = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + expect(seededkey).to.be.an.instanceof(ed25519.Ed25519PrivateKey) + const digest = await seededkey.hash() + expect(digest).to.have.length(34) + }) + + it('generates the same key from the same seed', async () => { + const seed = crypto.randomBytes(32) + const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + expect(seededkey1.equals(seededkey2)).to.eql(true) + expect(seededkey1.public.equals(seededkey2.public)).to.eql(true) + }) + + it('generates different keys for different seeds', async () => { + const seed1 = crypto.randomBytes(32) + const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed1, 512) + const seed2 = crypto.randomBytes(32) + const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed2, 512) + expect(seededkey1.equals(seededkey2)).to.eql(false) + expect(seededkey1.public.equals(seededkey2.public)).to.eql(false) + }) + + it('signs', async () => { + const text = crypto.randomBytes(512) + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.be.eql(true) + }) + + it('encoding', () => { + const keyMarshal = key.marshal() + const key2 = ed25519.unmarshalEd25519PrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = ed25519.unmarshalEd25519PublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.verify.privateKey) + const id = await key.id() + expect(id).to.eql('12D3KooWLqLxEfJ9nDdEe8Kh8PFvNPQRYDQBwyL7CMM7HhVd5LsX') + }) + + it('should export a password encrypted libp2p-key', async () => { + const key = await crypto.keys.generateKeyPair('Ed25519') + + if (!(key instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + const encryptedKey = await key.export('my secret') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, 'my secret') + + if (!(importedKey instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should export a libp2p-key with no password to encrypt', async () => { + const key = await crypto.keys.generateKeyPair('Ed25519') + + if (!(key instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + const encryptedKey = await key.export('') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, '') + + if (!(importedKey instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should fail to import libp2p-key with wrong password', async () => { + const key = await crypto.keys.generateKeyPair('Ed25519') + const encryptedKey = await key.export('my secret', 'libp2p-key') + try { + await crypto.keys.importKey(encryptedKey, 'not my secret') + } catch (err) { + expect(err).to.exist() + return + } + expect.fail('should have thrown') + }) + + describe('key equals', () => { + it('equals itself', () => { + expect( + key.equals(key) + ).to.eql( + true + ) + + expect( + key.public.equals(key.public) + ).to.eql( + true + ) + }) + + it('not equals other key', async () => { + const key2 = await crypto.keys.generateKeyPair('Ed25519', 512) + + if (!(key2 instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('sign and verify from seed', async () => { + const seed = new Uint8Array(32).fill(1) + const seededkey = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed) + const data = uint8ArrayFromString('hello world') + const sig = await seededkey.sign(data) + const valid = await seededkey.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('fails to verify for different data', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + expect(valid).to.be.eql(false) + }) + + describe('throws error instead of crashing', () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + testGarbage('key.verify', key.verify.bind(key), 2) + testGarbage('crypto.keys.unmarshalPrivateKey', crypto.keys.unmarshalPrivateKey.bind(crypto.keys)) + }) + + describe('go interop', () => { + // @ts-check + it('verifies with data from go', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + const ok = await key.verify(fixtures.verify.data, fixtures.verify.signature) + expect(ok).to.eql(true) + }) + + it('does not include the redundant public key when marshalling privatekey', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.redundantPubKey.privateKey) + const bytes = key.marshal() + expect(bytes.length).to.equal(64) + expect(bytes.subarray(32)).to.eql(key.public.marshal()) + }) + + it('verifies with data from go with redundant public key', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.redundantPubKey.publicKey) + const ok = await key.verify(fixtures.redundantPubKey.data, fixtures.redundantPubKey.signature) + expect(ok).to.eql(true) + }) + + it('generates the same signature as go', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.verify.privateKey) + const sig = await key.sign(fixtures.verify.data) + expect(sig).to.eql(fixtures.verify.signature) + }) + + it('generates the same signature as go with redundant public key', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.redundantPubKey.privateKey) + const sig = await key.sign(fixtures.redundantPubKey.data) + expect(sig).to.eql(fixtures.redundantPubKey.signature) + }) + }) +}) diff --git a/packages/crypto/test/keys/ephemeral-keys.spec.ts b/packages/crypto/test/keys/ephemeral-keys.spec.ts new file mode 100644 index 0000000000..1caadbc2db --- /dev/null +++ b/packages/crypto/test/keys/ephemeral-keys.spec.ts @@ -0,0 +1,62 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import * as crypto from '../../src/index.js' +import fixtures from '../fixtures/go-elliptic-key.js' + +const curves = ['P-256', 'P-384'] // 'P-521' fails in tests :( no clue why +const lengths: Record = { + 'P-256': 65, + 'P-384': 97, + 'P-521': 133 +} + +const secretLengths: Record = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 +} + +describe('generateEphemeralKeyPair', () => { + curves.forEach((curve) => { + it(`generate and shared key ${curve}`, async () => { + const keys = await Promise.all([ + crypto.keys.generateEphemeralKeyPair(curve), + crypto.keys.generateEphemeralKeyPair(curve) + ]) + + expect(keys[0].key).to.have.length(lengths[curve]) + expect(keys[1].key).to.have.length(lengths[curve]) + + const shared = await keys[0].genSharedKey(keys[1].key) + expect(shared).to.have.length(secretLengths[curve]) + }) + }) + + describe('go interop', () => { + it('generates a shared secret', async () => { + const curve = fixtures.curve + + const keys = await Promise.all([ + crypto.keys.generateEphemeralKeyPair(curve), + crypto.keys.generateEphemeralKeyPair(curve) + ]) + + const alice = keys[0] + const bob = keys[1] + bob.key = fixtures.bob.public + + const secrets = await Promise.all([ + alice.genSharedKey(bob.key), + bob.genSharedKey(alice.key, fixtures.bob) + ]) + + expect(secrets[0]).to.eql(secrets[1]) + expect(secrets[0]).to.have.length(32) + }) + }) + + it('handles bad curve name', async () => { + await expect(crypto.keys.generateEphemeralKeyPair('bad name')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_CURVE') + }) +}) diff --git a/packages/crypto/test/keys/importer.spec.ts b/packages/crypto/test/keys/importer.spec.ts new file mode 100644 index 0000000000..5fe0e26427 --- /dev/null +++ b/packages/crypto/test/keys/importer.spec.ts @@ -0,0 +1,20 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { exporter } from '../../src/keys/exporter.js' +import { importer } from '../../src/keys/importer.js' + +describe('libp2p-crypto importer/exporter', function () { + it('roundtrips', async () => { + for (const password of ['', 'password']) { + const secret = new Uint8Array(32) + for (let i = 0; i < secret.length; i++) { + secret[i] = i + } + + const exported = await exporter(secret, password) + const imported = await importer(exported, password) + expect(imported).to.deep.equal(secret) + } + }) +}) diff --git a/packages/crypto/test/keys/key-stretcher.spec.ts b/packages/crypto/test/keys/key-stretcher.spec.ts new file mode 100644 index 0000000000..c0b902d36a --- /dev/null +++ b/packages/crypto/test/keys/key-stretcher.spec.ts @@ -0,0 +1,59 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import * as crypto from '../../src/index.js' +import fixtures from '../fixtures/go-stretch-key.js' +import type { ECDHKey } from '../../src/keys/interface.js' + +describe('keyStretcher', () => { + describe('generate', () => { + const ciphers = ['AES-128', 'AES-256', 'Blowfish'] as Array<'AES-128' | 'AES-256' | 'Blowfish'> + const hashes = ['SHA1', 'SHA256', 'SHA512'] as Array<'SHA1' | 'SHA256' | 'SHA512'> + let res: ECDHKey + let secret: Uint8Array + + before(async () => { + res = await crypto.keys.generateEphemeralKeyPair('P-256') + secret = await res.genSharedKey(res.key) + }) + + ciphers.forEach((cipher) => { + hashes.forEach((hash) => { + it(`${cipher} - ${hash}`, async () => { + const keys = await crypto.keys.keyStretcher(cipher, hash, secret) + expect(keys.k1).to.exist() + expect(keys.k2).to.exist() + }) + }) + }) + + it('handles invalid cipher type', () => { + // @ts-expect-error cipher name is invalid + return expect(crypto.keys.keyStretcher('invalid-cipher', 'SHA256', 'secret')).to.eventually.be.rejected().with.property('code', 'ERR_INVALID_CIPHER_TYPE') + }) + + it('handles missing hash type', () => { + // @ts-expect-error secret name is invalid + return expect(crypto.keys.keyStretcher('AES-128', undefined, 'secret')).to.eventually.be.rejected().with.property('code', 'ERR_MISSING_HASH_TYPE') + }) + }) + + describe('go interop', () => { + fixtures.forEach((test) => { + it(`${test.cipher} - ${test.hash}`, async () => { + const cipher = test.cipher + const hash = test.hash + const secret = test.secret + const keys = await crypto.keys.keyStretcher(cipher, hash, secret) + + expect(keys.k1.iv).to.be.eql(test.k1.iv) + expect(keys.k1.cipherKey).to.be.eql(test.k1.cipherKey) + expect(keys.k1.macKey).to.be.eql(test.k1.macKey) + + expect(keys.k2.iv).to.be.eql(test.k2.iv) + expect(keys.k2.cipherKey).to.be.eql(test.k2.cipherKey) + expect(keys.k2.macKey).to.be.eql(test.k2.macKey) + }) + }) + }) +}) diff --git a/packages/crypto/test/keys/rsa.spec.ts b/packages/crypto/test/keys/rsa.spec.ts new file mode 100644 index 0000000000..27c9740b54 --- /dev/null +++ b/packages/crypto/test/keys/rsa.spec.ts @@ -0,0 +1,414 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' +import { RsaPrivateKey } from '../../src/keys/rsa-class.js' +import fixtures from '../fixtures/go-key-rsa.js' +import { testGarbage } from '../helpers/test-garbage-error-handling.js' + +const rsa = crypto.keys.supportedKeys.rsa + +/** @typedef {import('libp2p-crypto').keys.supportedKeys.rsa.RsaPrivateKey} RsaPrivateKey */ + +describe('RSA', function () { + this.timeout(20 * 1000) + let key: RsaPrivateKey + + before(async () => { + key = await rsa.generateKeyPair(512) + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(rsa.RsaPrivateKey) + const digest = await key.hash() + expect(digest).to.have.length(34) + }) + + it('signs', async () => { + const text = key.genSecret() + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.be.eql(true) + }) + + it('encoding', async () => { + const keyMarshal = key.marshal() + const key2 = await rsa.unmarshalRsaPrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = rsa.unmarshalRsaPublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const key = await crypto.keys.unmarshalPrivateKey(uint8ArrayFromString('CAASqAkwggSkAgEAAoIBAQCk0O+6oNRxhcdZe2GxEDrFBkDV4TZFZnp2ly/dL1cGMBql/8oXPZgei6h7+P5zzfDq2YCfwbjbf0IVY1AshRl6B5VGE1WS+9p1y1OZxJf5os6V1ENnTi6FTcyuBl4BN8dmIKOif0hqgqflaT5OhfYZDXfbJyVQj4vb2+Stu2Xpph3nwqAnTw/7GC/7jrt2Cq6Tu1PoZi36wSwEPYW3eQ1HAYxZjTYYDXl2iyHygnTcbkGRwAQ7vjk+mW7u60zyoolCm9f6Y7c/orJ33DDUocbaGJLlHcfd8bioBwaZy/2m7q43X8pQs0Q1/iwUt0HHZj1YARmHKbh0zR31ciFiV37dAgMBAAECggEADtJBNKnA4QKURj47r0YT2uLwkqtBi6UnDyISalQXAdXyl4n0nPlrhBewC5H9I+HZr+zmTbeIjaiYgz7el1pSy7AB4v7bG7AtWZlyx6mvtwHGjR+8/f3AXjl8Vgv5iSeAdXUq8fJ7SyS7v3wi38HZOzCEXj9bci6ud5ODMYJgLE4gZD0+i1+/V9cpuYfGpS/gLTLEMQLiw/9o8NSZ7sAnxg0UlYhotqaQY23hvXPBOe+0oa95zl2n6XTxCafa3dQl/B6CD1tUq9dhbQew4bxqMq/mhRO9pREEqZ083Uh+u4PTc1BeHgIQaS864pHPb+AY1F7KDvPtHhdojnghp8d70QKBgQDeRYFxo6sd04ohY86Z/i9icVYIyCvfXAKnaMKeGUjK7ou6sDJwFX8W97+CzXpZ/vffsk/l5GGhC50KqrITxHAy/h5IjyDODfps7NMIp0Dm9sO4PWibbw3OOVBRc8w3b3i7I8MrUUA1nLHE1T1HA1rKOTz5jYhE0fi9XKiT1ciKOQKBgQC903w+n9y7M7eaMW7Z5/13kZ7PS3HlM681eaPrk8J4J+c6miFF40/8HOsmarS38v0fgTeKkriPz5A7aLzRHhSiOnp350JNM6c3sLwPEs2qx/CRuWWx1rMERatfDdUH6mvlK6QHu0QgSfQR27EO6a6XvVSJXbvFmimjmtIaz/IpxQKBgQDWJ9HYVAGC81abZTaiWK3/A4QJYhQjWNuVwPICsgnYvI4Uib+PDqcs0ffLZ38DRw48kek5bxpBuJbOuDhro1EXUJCNCJpq7jzixituovd9kTRyR3iKii2bDM2+LPwOTXDdnk9lZRugjCEbrPkleq33Ob7uEtfAty4aBTTHe6uEwQKBgQCB+2q8RyMSXNuADhFlzOFXGrOwJm0bEUUMTPrduRQUyt4e1qOqA3klnXe3mqGcxBpnlEe/76/JacvNom6Ikxx16a0qpYRU8OWz0KU1fR6vrrEgV98241k5t6sdL4+MGA1Bo5xyXtzLb1hdUh3vpDwVU2OrnC+To3iXus/b5EBiMQKBgEI1OaBcFiyjgLGEyFKoZbtzH1mdatTExfrAQqCjOVjQByoMpGhHTXwEaosvyYu63Pa8AJPT7juSGaiKYEJFcXO9BiNyVfmQiqSHJcYeuh+fmO9IlHRHgy5xaIIC00AHS2vC/gXwmXAdPis6BZqDJeiCuOLWJ94QXn8JBT8IgGAI', 'base64pad')) + const id = await key.id() + expect(id).to.eql('QmQgsppVMDUpe83wcAqaemKbYvHeF127gnSFQ1xFnBodVw') + }) + + describe('key equals', () => { + it('equals itself', () => { + expect(key.equals(key)).to.eql(true) + + expect(key.public.equals(key.public)).to.eql(true) + }) + + it('not equals other key', async () => { + const key2 = await crypto.keys.generateKeyPair('RSA', 512) + + if (!(key2 instanceof RsaPrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.be.eql(true) + }) + + it('encrypt and decrypt', () => { + const data = uint8ArrayFromString('hello world') + const enc = key.public.encrypt(data) + const dec = key.decrypt(enc) + expect(dec).to.be.eql(data) + }) + + it('encrypt decrypt browser/node interop', async () => { + // @ts-check + /** + * @type {any} + */ + const id = await crypto.keys.unmarshalPrivateKey(uint8ArrayFromString('CAASqAkwggSkAgEAAoIBAQCk0O+6oNRxhcdZe2GxEDrFBkDV4TZFZnp2ly/dL1cGMBql/8oXPZgei6h7+P5zzfDq2YCfwbjbf0IVY1AshRl6B5VGE1WS+9p1y1OZxJf5os6V1ENnTi6FTcyuBl4BN8dmIKOif0hqgqflaT5OhfYZDXfbJyVQj4vb2+Stu2Xpph3nwqAnTw/7GC/7jrt2Cq6Tu1PoZi36wSwEPYW3eQ1HAYxZjTYYDXl2iyHygnTcbkGRwAQ7vjk+mW7u60zyoolCm9f6Y7c/orJ33DDUocbaGJLlHcfd8bioBwaZy/2m7q43X8pQs0Q1/iwUt0HHZj1YARmHKbh0zR31ciFiV37dAgMBAAECggEADtJBNKnA4QKURj47r0YT2uLwkqtBi6UnDyISalQXAdXyl4n0nPlrhBewC5H9I+HZr+zmTbeIjaiYgz7el1pSy7AB4v7bG7AtWZlyx6mvtwHGjR+8/f3AXjl8Vgv5iSeAdXUq8fJ7SyS7v3wi38HZOzCEXj9bci6ud5ODMYJgLE4gZD0+i1+/V9cpuYfGpS/gLTLEMQLiw/9o8NSZ7sAnxg0UlYhotqaQY23hvXPBOe+0oa95zl2n6XTxCafa3dQl/B6CD1tUq9dhbQew4bxqMq/mhRO9pREEqZ083Uh+u4PTc1BeHgIQaS864pHPb+AY1F7KDvPtHhdojnghp8d70QKBgQDeRYFxo6sd04ohY86Z/i9icVYIyCvfXAKnaMKeGUjK7ou6sDJwFX8W97+CzXpZ/vffsk/l5GGhC50KqrITxHAy/h5IjyDODfps7NMIp0Dm9sO4PWibbw3OOVBRc8w3b3i7I8MrUUA1nLHE1T1HA1rKOTz5jYhE0fi9XKiT1ciKOQKBgQC903w+n9y7M7eaMW7Z5/13kZ7PS3HlM681eaPrk8J4J+c6miFF40/8HOsmarS38v0fgTeKkriPz5A7aLzRHhSiOnp350JNM6c3sLwPEs2qx/CRuWWx1rMERatfDdUH6mvlK6QHu0QgSfQR27EO6a6XvVSJXbvFmimjmtIaz/IpxQKBgQDWJ9HYVAGC81abZTaiWK3/A4QJYhQjWNuVwPICsgnYvI4Uib+PDqcs0ffLZ38DRw48kek5bxpBuJbOuDhro1EXUJCNCJpq7jzixituovd9kTRyR3iKii2bDM2+LPwOTXDdnk9lZRugjCEbrPkleq33Ob7uEtfAty4aBTTHe6uEwQKBgQCB+2q8RyMSXNuADhFlzOFXGrOwJm0bEUUMTPrduRQUyt4e1qOqA3klnXe3mqGcxBpnlEe/76/JacvNom6Ikxx16a0qpYRU8OWz0KU1fR6vrrEgV98241k5t6sdL4+MGA1Bo5xyXtzLb1hdUh3vpDwVU2OrnC+To3iXus/b5EBiMQKBgEI1OaBcFiyjgLGEyFKoZbtzH1mdatTExfrAQqCjOVjQByoMpGhHTXwEaosvyYu63Pa8AJPT7juSGaiKYEJFcXO9BiNyVfmQiqSHJcYeuh+fmO9IlHRHgy5xaIIC00AHS2vC/gXwmXAdPis6BZqDJeiCuOLWJ94QXn8JBT8IgGAI', 'base64pad')) + + if (!(id instanceof RsaPrivateKey)) { + throw new Error('Key was incorrect type') + } + + const msg = uint8ArrayFromString('hello') + + // browser + const dec1 = id.decrypt(uint8ArrayFromString('YRFUDx8UjbWSfDS84cDA4WowaaOmd1qFNAv5QutodCKYb9uPtU/tDiAvJzOGu5DCJRo2J0l/35P2weiB4/C2Cb1aZgXKMx/QQC+2jSJiymhqcZaYerjTvkCFwkjCaqthoVo/YXxsaFZ1q7bdTZUDH1TaJR7hWfSyzyPcA8c0w43MIsw16pY8ZaPSclvnCwhoTg1JGjMk6te3we7+wR8QU7VrPhs54mZWxrpu3NQ8xZ6xQqIedsEiNhBUccrCSzYghgsP0Ae/8iKyGyl3U6IegsJNn8jcocvzOJrmU03rgIFPjvuBdaqB38xDSTjbA123KadB28jNoSZh18q/yH3ZIg==', 'base64pad')) + expect(dec1).to.be.eql(msg) + // node + const dec2 = id.decrypt(uint8ArrayFromString('e6yxssqXsWc27ozDy0PGKtMkCS28KwFyES2Ijz89yiz+w6bSFkNOhHPKplpPzgQEuNoUGdbseKlJFyRYHjIT8FQFBHZM8UgSkgoimbY5on4xSxXs7E5/+twjqKdB7oNveTaTf7JCwaeUYnKSjbiYFEawtMiQE91F8sTT7TmSzOZ48tUhnddAAZ3Ac/O3Z9MSAKOCDipi+JdZtXRT8KimGt36/7hjjosYmPuHR1Xy/yMTL6SMbXtBM3yAuEgbQgP+q/7kHMHji3/JvTpYdIUU+LVtkMusXNasRA+UWG2zAht18vqjFMsm9JTiihZw9jRHD4vxAhf75M992tnC+0ZuQg==', 'base64pad')) + expect(dec2).to.be.eql(msg) + }) + + it('fails to verify for different data', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + expect(valid).to.be.eql(false) + }) + + describe('export and import', () => { + it('password protected PKCS #8', async () => { + const pem = await key.export('my secret', 'pkcs-8') + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + const clone = await crypto.keys.importKey(pem, 'my secret') + + if (!(clone instanceof RsaPrivateKey)) { + throw new Error('Wrong kind of key imported') + } + + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + }) + + it('defaults to PKCS #8', async () => { + const pem = await key.export('another secret') + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + const clone = await crypto.keys.importKey(pem, 'another secret') + + if (!(clone instanceof RsaPrivateKey)) { + throw new Error('Wrong kind of key imported') + } + + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + }) + + it('should export a password encrypted libp2p-key', async () => { + const encryptedKey = await key.export('my secret', 'libp2p-key') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, 'my secret') + + if (!(importedKey instanceof RsaPrivateKey)) { + throw new Error('Wrong kind of key imported') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should fail to import libp2p-key with wrong password', async () => { + const encryptedKey = await key.export('my secret', 'libp2p-key') + try { + await crypto.keys.importKey(encryptedKey, 'not my secret') + } catch (err) { + expect(err).to.exist() + return + } + expect.fail('should have thrown') + }) + + it('needs correct password', async () => { + const pem = await key.export('another secret') + try { + await crypto.keys.importKey(pem, 'not the secret') + } catch (err) { + return // expected + } + throw new Error('Expected error to be thrown') + }) + + it('handles invalid export type', () => { + return expect(key.export('secret', 'invalid-type')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_EXPORT_FORMAT') + }) + }) + + describe('throws error instead of crashing', () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + testGarbage('key.verify', key.verify.bind(key), 2, true) + testGarbage( + 'crypto.keys.unmarshalPrivateKey', + crypto.keys.unmarshalPrivateKey.bind(crypto.keys) + ) + }) + + describe('go interop', () => { + it('verifies with data from go', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + const ok = await key.verify(fixtures.verify.data, fixtures.verify.signature) + expect(ok).to.equal(true) + }) + }) + + describe('openssl interop', () => { + it('can read a private key', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:3072 + * -pkeyopt rsa_keygen_pubexp:65537 + */ + const pem = `-----BEGIN PRIVATE KEY----- +MIIG/wIBADANBgkqhkiG9w0BAQEFAASCBukwggblAgEAAoIBgQDp0Whyqa8KmdvK +0MsQGJEBzDAEHAZc0C6cr0rkb6Xwo+yB5kjZBRDORk0UXtYGE1pYt4JhUTmMzcWO +v2xTIsdbVMQlNtput2U8kIqS1cSTkX5HxOJtCiIzntMzuR/bGPSOexkyFQ8nCUqb +ROS7cln/ixprra2KMAKldCApN3ue2jo/JI1gyoS8sekhOASAa0ufMPpC+f70sc75 +Y53VLnGBNM43iM/2lsK+GI2a13d6rRy86CEM/ygnh/EDlyNDxo+SQmy6GmSv/lmR +xgWQE2dIfK504KIxFTOphPAQAr9AsmcNnCQLhbz7YTsBz8WcytHGQ0Z5pnBQJ9AV +CX9E6DFHetvs0CNLVw1iEO06QStzHulmNEI/3P8I1TIxViuESJxSu3pSNwG1bSJZ ++Qee24vvlz/slBzK5gZWHvdm46v7vl5z7SA+whncEtjrswd8vkJk9fI/YTUbgOC0 +HWMdc2t/LTZDZ+LUSZ/b2n5trvdJSsOKTjEfuf0wICC08pUUk8MCAwEAAQKCAYEA +ywve+DQCneIezHGk5cVvp2/6ApeTruXalJZlIxsRr3eq2uNwP4X2oirKpPX2RjBo +NMKnpnsyzuOiu+Pf3hJFrTpfWzHXXm5Eq+OZcwnQO5YNY6XGO4qhSNKT9ka9Mzbo +qRKdPrCrB+s5rryVJXKYVSInP3sDSQ2IPsYpZ6GW6Mv56PuFCpjTzElzejV7M0n5 +0bRmn+MZVMVUR54KYiaCywFgUzmr3yfs1cfcsKqMRywt2J58lRy/chTLZ6LILQMv +4V01neVJiRkTmUfIWvc1ENIFM9QJlky9AvA5ASvwTTRz8yOnxoOXE/y4OVyOePjT +cz9eumu9N5dPuUIMmsYlXmRNaeGZPD9bIgKY5zOlfhlfZSuOLNH6EHBNr6JAgfwL +pdP43sbg2SSNKpBZ0iSMvpyTpbigbe3OyhnFH/TyhcC2Wdf62S9/FRsvjlRPbakW +YhKAA2kmJoydcUDO5ccEga8b7NxCdhRiczbiU2cj70pMIuOhDlGAznyxsYbtyxaB +AoHBAPy6Cbt6y1AmuId/HYfvms6i8B+/frD1CKyn+sUDkPf81xSHV7RcNrJi1S1c +V55I0y96HulsR+GmcAW1DF3qivWkdsd/b4mVkizd/zJm3/Dm8p8QOnNTtdWvYoEB +VzfAhBGaR/xflSLxZh2WE8ZHQ3IcRCXV9ZFgJ7PMeTprBJXzl0lTptvrHyo9QK1v +obLrL/KuXWS0ql1uSnJr1vtDI5uW8WU4GDENeU5b/CJHpKpjVxlGg+7pmLknxlBl +oBnZnQKBwQDs2Ky29qZ69qnPWowKceMJ53Z6uoUeSffRZ7xuBjowpkylasEROjuL +nyAihIYB7fd7R74CnRVYLI+O2qXfNKJ8HN+TgcWv8LudkRcnZDSvoyPEJAPyZGfr +olRCXD3caqtarlZO7vXSAl09C6HcL2KZ8FuPIEsuO0Aw25nESMg9eVMaIC6s2eSU +NUt6xfZw1JC0c+f0LrGuFSjxT2Dr5WKND9ageI6afuauMuosjrrOMl2g0dMcSnVz +KrtYa7Wi1N8CgcBFnuJreUplDCWtfgEen40f+5b2yAQYr4fyOFxGxdK73jVJ/HbW +wsh2n+9mDZg9jIZQ/+1gFGpA6V7W06dSf/hD70ihcKPDXSbloUpaEikC7jxMQWY4 +uwjOkwAp1bq3Kxu21a+bAKHO/H1LDTrpVlxoJQ1I9wYtRDXrvBpxU2XyASbeFmNT +FhSByFn27Ve4OD3/NrWXtoVwM5/ioX6ZvUcj55McdTWE3ddbFNACiYX9QlyOI/TY +bhWafDCPmU9fj6kCgcEAjyQEfi9jPj2FM0RODqH1zS6OdG31tfCOTYicYQJyeKSI +/hAezwKaqi9phHMDancfcupQ89Nr6vZDbNrIFLYC3W+1z7hGeabMPNZLYAs3rE60 +dv4tRHlaNRbORazp1iTBmvRyRRI2js3O++3jzOb2eILDUyT5St+UU/LkY7R5EG4a +w1df3idx9gCftXufDWHqcqT6MqFl0QgIzo5izS68+PPxitpRlR3M3Mr4rCU20Rev +blphdF+rzAavYyj1hYuRAoHBANmxwbq+QqsJ19SmeGMvfhXj+T7fNZQFh2F0xwb2 +rMlf4Ejsnx97KpCLUkoydqAs2q0Ws9Nkx2VEVx5KfUD7fWhgbpdnEPnQkfeXv9sD +vZTuAoqInN1+vj1TME6EKR/6D4OtQygSNpecv23EuqEvyXWqRVsRt9Qd2B0H4k7h +gnjREs10u7zyqBIZH7KYVgyh27WxLr859ap8cKAH6Fb+UOPtZo3sUeeume60aebn +4pMwXeXP+LO8NIfRXV8mgrm86g== +-----END PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, '') + expect(key).to.exist() + const id = await key.id() + expect(id).to.equal('QmfWu2Xp8DZzCkZZzoPB9rcrq4R4RZid6AWE6kmrUAzuHy') + }) + + // AssertionError: expected 'this only supports pkcs5PBES2' to not exist + it.skip('can read a private encrypted key (v1)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQI2563Jugj/KkCAggABIICgPxHkKtUUE8EWevq +eX9nTjqpbsv0QoXQMhegfxDELJLU8tj6V0bWNt7QDdfQ1n6FRgnNvNGick6gyqHH +yH9qC2oXwkDFP7OrHp2NEZd7DHQLLc+L4KJ/0dzsiZ1U9no7XzQMUay9Bc918ADE +pN2/EqigWkaG4gNjkAeKWr6+BNRevDXlSvls7YDboNcTiACi5zJkthivB9g3vT1m +gPdN6Gf/mmqtBTDHeqj5QsmXYqeCyo5b26JgYsziABVZDHph4ekPUsTvudRpE9Ex +baXwdYEAZxVpSbTvQ3A5qysjSZeM9ttfRTSSwL391q7dViz4+aujpk0Vj7piH+1B +CkfO8/XudRdRlnOe+KjMidktKCsMGCIOW92IlfMvIQ/Zn1GTYj9bRXONFNJ2WPND +UmCKnL7cmworwg/weRorrGKBWIGspU+tDASOPSvIGKo6Hoxm4CN1TpDRY7DAGlgm +Y3TEbMYfpXyzkPjvAhJDt03D3J9PrTO6uM5d7YUaaTmJ2TQFQVF2Lc3Uz8lDJLs0 +ZYtfQ/4H+YY2RrX7ua7t6ArUcYXZtv0J4lRYWjwV8fGPUVc0d8xLJU0Yjf4BD7K8 +rsavHo9b5YvBUX7SgUyxAEembEOe3SjQ+gPu2U5wovcjUuC9eItEEsXGrx30BQ0E +8BtK2+hp0eMkW5/BYckJkH+Yl8ypbzRGRRIZzLgeI4JveSx/mNhewfgTr+ORPThZ +mBdkD5r+ixWF174naw53L8U9wF8kiK7pIE1N9TR4USEeovLwX6Ni/2MMDZedOfof +2f77eUdLsK19/5/lcgAAYaXauXWhy2d2r3SayFrC9woy0lh2VLKRMBjcx1oWb7dp +0uxzo5Y= +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 aes-128-cbc)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 aes-128-cbc -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIP5QK2RfqUl4CAggA +MB0GCWCGSAFlAwQBAgQQj3OyM9gnW2dd/eRHkxjGrgSCAoCpM5GZB0v27cxzZsGc +O4/xqgwB0c/bSJ6QogtYU2KVoc7ZNQ5q9jtzn3I4ONvneOkpm9arzYz0FWnJi2C3 +BPiF0D1NkfvjvMLv56bwiG2A1oBECacyAb2pXYeJY7SdtYKvcbgs3jx65uCm6TF2 +BylteH+n1ewTQN9DLfASp1n81Ajq9lQGaK03SN2MUtcAPp7N9gnxJrlmDGeqlPRs +KpQYRcot+kE6Ew8a5jAr7mAxwpqvr3SM4dMvADZmRQsM4Uc/9+YMUdI52DG87EWc +0OUB+fnQ8jw4DZgOE9KKM5/QTWc3aEw/dzXr/YJsrv01oLazhqVHnEMG0Nfr0+DP +q+qac1AsCsOb71VxaRlRZcVEkEfAq3gidSPD93qmlDrCnmLYTilcLanXUepda7ez +qhjkHtpwBLN5xRZxOn3oUuLGjk8VRwfmFX+RIMYCyihjdmbEDYpNUVkQVYFGi/F/ +1hxOyl9yhGdL0hb9pKHH10GGIgoqo4jSTLlb4ennihGMHCjehAjLdx/GKJkOWShy +V9hj8rAuYnRNb+tUW7ChXm1nLq14x9x1tX0ciVVn3ap/NoMkbFTr8M3pJ4bQlpAn +wCT2erYqwQtgSpOJcrFeph9TjIrNRVE7Zlmr7vayJrB/8/oPssVdhf82TXkna4fB +PcmO0YWLa117rfdeNM/Duy0ThSdTl39Qd+4FxqRZiHjbt+l0iSa/nOjTv1TZ/QqF +wqrO6EtcM45fbFJ1Y79o2ptC2D6MB4HKJq9WCt064/8zQCVx3XPbb3X8Z5o/6koy +ePGbz+UtSb9xczvqpRCOiFLh2MG1dUgWuHazjOtUcVWvilKnkjCMzZ9s1qG0sUDj +nPyn +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 aes-256-cbc)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 aes-256-cbc -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIhuL894loRucCAggA +MB0GCWCGSAFlAwQBKgQQEoEtsjW3iC9/u0uGvkxX7wSCAoAsX3l6JoR2OGbT8CkY +YT3RQFqquOgItYOHw6E3tir2YrmxEAo99nxoL8pdto37KSC32eAGnfv5R1zmHHSx +0M3/y2AWiCBTX95EEzdtGC1hK3PBa/qpp/xEmcrsjYN6NXxMAkhC0hMP/HdvqMAg +ee7upvaYJsJcl8QLFNayAWr8b8cZA/RBhGEIRl59Eyj6nNtxDt3bCrfe06o1CPCV +50/fRZEwFOi/C6GYvPN6MrPZO3ALBWgopLT2yQqycTKtfxYWIdOsMBkAjKf2D6Pk +u2mqBsaP4b71jIIeT4euSJLsoJV+O39s8YHXtW8GtOqp7V5kIlnm90lZ9wzeLTZ7 +HJsD/jEdYto5J3YWm2wwEDccraffJSm7UDtJBvQdIx832kxeFCcGQjW38Zl1qqkg +iTH1PLTypxj2ZuviS2EkXVFb/kVU6leWwOt6fqWFC58UvJKeCk/6veazz3PDnTWM +92ClUqFd+CZn9VT4CIaJaAc6v5NLpPp+T9sRX9AtequPm7FyTeevY9bElfyk9gW9 +JDKgKxs6DGWDa16RL5vzwtU+G3o6w6IU+mEwa6/c+hN+pRFs/KBNLLSP9OHBx7BJ +X/32Ft+VFhJaK+lQ+f+hve7od/bgKnz4c/Vtp7Dh51DgWgCpBgb8p0vqu02vTnxD +BXtDv3h75l5PhvdWfVIzpMWRYFvPR+vJi066FjAz2sjYc0NMLSYtZWyWoIInjhoX +Dp5CQujCtw/ZSSlwde1DKEWAW4SeDZAOQNvuz0rU3eosNUJxEmh3aSrcrRtDpw+Y +mBUuWAZMpz7njBi7h+JDfmSW/GAaMwrVFC2gef5375R0TejAh+COAjItyoeYEvv8 +DQd8 +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 des)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 des -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICwzA9BgkqhkiG9w0BBQ0wMDAbBgkqhkiG9w0BBQwwDgQI0lXp62ozXvwCAggA +MBEGBSsOAwIHBAiR3Id5vH0u4wSCAoDQQYOrrkPFPIa0S5fQGXnJw1F/66g92Gs1 +TkGydn4ouabWb++Vbi2chee1oyZsN2l8YNzDi0Gb2PfjsGpg2aJk0a3/efgA0u6T +leEH1dA/7Hr9NVspgHkaXpHt3X6wdbznLYJeAelfj7sDXpOkULGWCkCst0Txb6bi +Oxv4c0yYykiuUrp+2xvHbF9c2PrcDb58u/OBZcCg3QB1gTugQKM+ZIBRhcTEFLrm +8gWbzBfwYiUm6aJce4zoafP0NSlEOBbpbr73A08Q1IK6pISwltOUhhTvspSZnK41 +y2CHt5Drnpl1pfOw9Q0svO3VrUP+omxP1SFP17ZfaRGw2uHd08HJZs438x5dIQoH +QgjlZ8A5rcT3FjnytSh3fln2ZxAGuObghuzmOEL/+8fkGER9QVjmQlsL6OMfB4j4 +ZAkLf74uaTdegF3SqDQaGUwWgk7LyualmUXWTBoeP9kRIsRQLGzAEmd6duBPypED +HhKXP/ZFA1kVp3x1fzJ2llMFB3m1JBwy4PiohqrIJoR+YvKUvzVQtbOjxtCEAj87 +JFnlQj0wjTd6lfNn+okewMNjKINZx+08ui7XANNU/l18lHIIz3ssXJSmqMW+hRZ9 +9oB2tntLrnRMhkVZDVHadq7eMFOPu0rkekuaZm9CO2vu4V7Qa2h+gOoeczYza0H7 +A+qCKbprxyL8SKI5vug2hE+mfC1leXVRtUYm1DnE+oet99bFd0fN20NwTw0rOeRg +0Z+/ZpQNizrXxfd3sU7zaJypWCxZ6TD/U/AKBtcb2gqmUjObZhbfbWq6jU2Ye//w +EBqQkwAUXR1tNekF8CWLOrfC/wbLRxVRkayb8bQUfdgukLpz0bgw +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 des3)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 des3 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISznrfHd+D58CAggA +MBQGCCqGSIb3DQMHBAhx0DnnUvDiHASCAoCceplm+Cmwlgvn4hNsv6e4c/S1iA7w +2hU7Jt8JgRCIMWjP2FthXOAFLa2fD4g3qncYXcDAFBXNyoh25OgOwstO14YkxhDi +wG4TeppGUt9IlyyCol6Z4WhQs1TGm5OcD5xDta+zBXsBnlgmKLD5ZXPEYB+3v/Dg +SvM4sQz6NgkVHN52hchERsnknwSOghiK9mIBH0RZU5LgzlDy2VoBCiEPVdZ7m4F2 +dft5e82zFS58vwDeNN/0r7fC54TyJf/8k3q94+4Hp0mseZ67LR39cvnEKuDuFROm +kLPLekWt5R2NGdunSQlA79BkrNB1ADruO8hQOOHMO9Y3/gNPWLKk+qrfHcUni+w3 +Ofq+rdfakHRb8D6PUmsp3wQj6fSOwOyq3S50VwP4P02gKcZ1om1RvEzTbVMyL3sh +hZcVB3vViu3DO2/56wo29lPVTpj9bSYjw/CO5jNpPBab0B/Gv7JAR0z4Q8gn6OPy +qf+ddyW4Kcb6QUtMrYepghDthOiS3YJV/zCNdL3gTtVs5Ku9QwQ8FeM0/5oJZPlC +TxGuOFEJnYRWqIdByCP8mp/qXS5alSR4uoYQSd7vZG4vkhkPNSAwux/qK1IWfqiW +3XlZzrbD//9IzFVqGRs4nRIFq85ULK0zAR57HEKIwGyn2brEJzrxpV6xsHBp+m4w +6r0+PtwuWA0NauTCUzJ1biUdH8t0TgBL6YLaMjlrfU7JstH3TpcZzhJzsjfy0+zV +NT2TO3kSzXpQ5M2VjOoHPm2fqxD/js+ThDB3QLi4+C7HqakfiTY1lYzXl9/vayt6 +DUD29r9pYL9ErB9tYko2rat54EY7k7Ts6S5jf+8G7Zz234We1APhvqaG +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + }) +}) diff --git a/packages/crypto/test/keys/secp256k1.spec.ts b/packages/crypto/test/keys/secp256k1.spec.ts new file mode 100644 index 0000000000..02e6ee77ad --- /dev/null +++ b/packages/crypto/test/keys/secp256k1.spec.ts @@ -0,0 +1,216 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' +import * as Secp256k1 from '../../src/keys/secp256k1-class.js' +import * as secp256k1Crypto from '../../src/keys/secp256k1.js' +import fixtures from '../fixtures/go-key-secp256k1.js' + +const secp256k1 = crypto.keys.supportedKeys.secp256k1 +const keysPBM = crypto.keys.keysPBM +const randomBytes = crypto.randomBytes + +describe('secp256k1 keys', () => { + let key: Secp256k1.Secp256k1PrivateKey + + before(async () => { + key = await secp256k1.generateKeyPair() + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + expect(key.public).to.be.an.instanceof(secp256k1.Secp256k1PublicKey) + + const digest = await key.hash() + expect(digest).to.have.length(34) + + const publicDigest = await key.public.hash() + expect(publicDigest).to.have.length(34) + }) + + it('optionally accepts a `bits` argument when generating a key', async () => { + const _key = await secp256k1.generateKeyPair() + expect(_key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + }) + + it('signs', async () => { + const text = randomBytes(512) + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.equal(true) + }) + + it('encoding', () => { + const keyMarshal = key.marshal() + const key2 = secp256k1.unmarshalSecp256k1PrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = secp256k1.unmarshalSecp256k1PublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + const key = secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data ?? new Uint8Array()) + const id = await key.id() + expect(id).to.eql('QmPCyMBGEyifPtx5aa6k6wkY9N1eBf9vHK1eKfNc35q9uq') + }) + + it('should export a password encrypted libp2p-key', async () => { + const key = await crypto.keys.generateKeyPair('secp256k1') + + if (!(key instanceof Secp256k1.Secp256k1PrivateKey)) { + throw new Error('Generated wrong key type') + } + + const encryptedKey = await key.export('my secret') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, 'my secret') + + if (!(importedKey instanceof Secp256k1.Secp256k1PrivateKey)) { + throw new Error('Imported wrong key type') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should fail to import libp2p-key with wrong password', async () => { + const key = await crypto.keys.generateKeyPair('secp256k1') + const encryptedKey = await key.export('my secret', 'libp2p-key') + + await expect(crypto.keys.importKey(encryptedKey, 'not my secret')).to.eventually.be.rejected() + }) + + describe('key equals', () => { + it('equals itself', () => { + expect(key.equals(key)).to.eql(true) + + expect(key.public.equals(key.public)).to.eql(true) + }) + + it('not equals other key', async () => { + const key2 = await secp256k1.generateKeyPair() + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('fails to verify for different data', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + expect(valid).to.eql(false) + }) +}) + +describe('crypto functions', () => { + let privKey: Uint8Array + let pubKey: Uint8Array + + before(() => { + privKey = secp256k1Crypto.generateKey() + pubKey = secp256k1Crypto.computePublicKey(privKey) + }) + + it('generates valid keys', () => { + expect(() => { + secp256k1Crypto.validatePrivateKey(privKey) + secp256k1Crypto.validatePublicKey(pubKey) + }).to.not.throw() + }) + + it('does not validate an invalid key', () => { + expect(() => { secp256k1Crypto.validatePublicKey(uint8ArrayFromString('42')) }).to.throw() + expect(() => { secp256k1Crypto.validatePrivateKey(uint8ArrayFromString('42')) }).to.throw() + }) + + it('validates a correct signature', async () => { + const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) + const valid = await secp256k1Crypto.hashAndVerify(pubKey, sig, uint8ArrayFromString('hello')) + expect(valid).to.equal(true) + }) + + it('does not validate when validating a message with an invalid signature', async () => { + const result = await secp256k1Crypto.hashAndVerify(pubKey, uint8ArrayFromString('invalid-sig'), uint8ArrayFromString('hello')) + + expect(result).to.be.false() + }) + + it('errors if given a null Uint8Array to sign', async () => { + // @ts-expect-error incorrect args + await expect(secp256k1Crypto.hashAndSign(privKey, null)).to.eventually.be.rejected() + }) + + it('errors when signing with an invalid key', async () => { + await expect(secp256k1Crypto.hashAndSign(uint8ArrayFromString('42'), uint8ArrayFromString('Hello'))).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_INPUT') + }) + + it('errors if given a null Uint8Array to validate', async () => { + const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) + + // @ts-expect-error incorrect args + await expect(secp256k1Crypto.hashAndVerify(privKey, sig, null)).to.eventually.be.rejected() + }) + + it('throws when compressing an invalid public key', () => { + expect(() => secp256k1Crypto.compressPublicKey(uint8ArrayFromString('42'))).to.throw() + }) + + it('throws when decompressing an invalid public key', () => { + expect(() => secp256k1Crypto.decompressPublicKey(uint8ArrayFromString('42'))).to.throw() + }) + + it('compresses/decompresses a valid public key', () => { + const decompressed = secp256k1Crypto.decompressPublicKey(pubKey) + expect(decompressed).to.exist() + expect(decompressed.length).to.be.eql(65) + const recompressed = secp256k1Crypto.compressPublicKey(decompressed) + expect(recompressed).to.eql(pubKey) + }) +}) + +describe('go interop', () => { + it('loads a private key marshaled by go-libp2p-crypto', () => { + // we need to first extract the key data from the protobuf, which is + // normally handled by js-libp2p-crypto + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data ?? new Uint8Array()) + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + expect(key.bytes).to.eql(fixtures.privateKey) + }) + + it('loads a public key marshaled by go-libp2p-crypto', () => { + const decoded = keysPBM.PublicKey.decode(fixtures.publicKey) + expect(decoded.Type).to.be.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PublicKey(decoded.Data ?? new Uint8Array()) + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PublicKey) + expect(key.bytes).to.eql(fixtures.publicKey) + }) + + it('generates the same signature as go-libp2p-crypto', async () => { + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data ?? new Uint8Array()) + const sig = await key.sign(fixtures.message) + expect(sig).to.eql(fixtures.signature) + }) +}) diff --git a/packages/crypto/test/random-bytes.spec.ts b/packages/crypto/test/random-bytes.spec.ts new file mode 100644 index 0000000000..06fa923b1e --- /dev/null +++ b/packages/crypto/test/random-bytes.spec.ts @@ -0,0 +1,22 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import randomBytes from '../src/random-bytes.js' + +describe('randomBytes', () => { + it('produces random bytes', () => { + expect(randomBytes(16)).to.have.length(16) + }) + + it('throws if length is 0', () => { + expect(() => randomBytes(0)).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') + }) + + it('throws if length is < 0', () => { + expect(() => randomBytes(-1)).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') + }) + + it('throws if length is not a number', () => { + // @ts-expect-error invalid params + expect(() => randomBytes('hi')).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') + }) +}) diff --git a/packages/crypto/test/util.spec.ts b/packages/crypto/test/util.spec.ts new file mode 100644 index 0000000000..84f6af0998 --- /dev/null +++ b/packages/crypto/test/util.spec.ts @@ -0,0 +1,34 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import 'node-forge/lib/jsbn.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import * as util from '../src/util.js' + +describe('Util', () => { + let bn: typeof forge.jsbn.BigInteger + + before(() => { + bn = new forge.jsbn.BigInteger('dead', 16) + }) + + it('should convert BigInteger to a uint base64url encoded string', () => { + expect(util.bigIntegerToUintBase64url(bn)).to.eql('3q0') + }) + + it('should convert BigInteger to a uint base64url encoded string with padding', () => { + const bnpad = new forge.jsbn.BigInteger('ff', 16) + expect(util.bigIntegerToUintBase64url(bnpad, 2)).to.eql('AP8') + }) + + it('should convert base64url encoded string to BigInteger', () => { + const num = util.base64urlToBigInteger('3q0') + expect(num.equals(bn)).to.be.true() + }) + + it('should convert base64url encoded string to Uint8Array with padding', () => { + const buf = util.base64urlToBuffer('AP8', 2) + expect(Uint8Array.from([0, 255])).to.eql(buf) + }) +}) diff --git a/packages/crypto/test/workaround.spec.ts b/packages/crypto/test/workaround.spec.ts new file mode 100644 index 0000000000..6b96c30b9d --- /dev/null +++ b/packages/crypto/test/workaround.spec.ts @@ -0,0 +1,26 @@ + +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { derivedEmptyPasswordKey } from '../src/ciphers/aes-gcm.browser.js' + +describe('Constant derived key is generated correctly', () => { + it('Generates correctly', async () => { + if ((typeof navigator !== 'undefined' && navigator.userAgent.includes('Safari')) || typeof crypto === 'undefined') { + // WebKit Linux can't generate this. Hence the workaround. + return + } + + const generatedKey = await crypto.subtle.exportKey('jwk', + await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } }, + await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']), + { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']) + ) + + // Webkit macos flips these. Sort them so they match. + derivedEmptyPasswordKey.key_ops.sort() + generatedKey?.key_ops?.sort() + + expect(generatedKey).to.eql(derivedEmptyPasswordKey) + }) +}) diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json new file mode 100644 index 0000000000..0051483a8d --- /dev/null +++ b/packages/crypto/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-keys" + }, + { + "path": "../interfaces" + } + ] +} diff --git a/packages/interface-address-manager/package.json b/packages/interface-address-manager/package.json index 9dacdafde4..7452506c40 100644 --- a/packages/interface-address-manager/package.json +++ b/packages/interface-address-manager/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -127,10 +42,10 @@ "build": "aegir build" }, "dependencies": { - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-compliance-tests/package.json b/packages/interface-compliance-tests/package.json index 3513dc667d..22123fb6ae 100644 --- a/packages/interface-compliance-tests/package.json +++ b/packages/interface-compliance-tests/package.json @@ -59,91 +59,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", diff --git a/packages/interface-connection-compliance-tests/package.json b/packages/interface-connection-compliance-tests/package.json index ebc9e3d97c..4630b98670 100644 --- a/packages/interface-connection-compliance-tests/package.json +++ b/packages/interface-connection-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", diff --git a/packages/interface-connection-encrypter-compliance-tests/package.json b/packages/interface-connection-encrypter-compliance-tests/package.json index c628c41665..2994e30dab 100644 --- a/packages/interface-connection-encrypter-compliance-tests/package.json +++ b/packages/interface-connection-encrypter-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -132,13 +47,13 @@ "@libp2p/interface-connection-encrypter": "^4.0.0", "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "aegir": "^39.0.5", - "it-all": "^3.0.1", + "it-all": "^3.0.2", "it-pair": "^2.0.2", "it-pipe": "^3.0.1", "it-stream-types": "^2.0.1", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-encrypter-compliance-tests/tsconfig.json b/packages/interface-connection-encrypter-compliance-tests/tsconfig.json index 77fd5244fb..e00ae1221b 100644 --- a/packages/interface-connection-encrypter-compliance-tests/tsconfig.json +++ b/packages/interface-connection-encrypter-compliance-tests/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../interface-peer-id" + }, + { + "path": "../peer-id-factory" } ] } diff --git a/packages/interface-connection-encrypter/package.json b/packages/interface-connection-encrypter/package.json index 5d511231e1..812ec5e4ed 100644 --- a/packages/interface-connection-encrypter/package.json +++ b/packages/interface-connection-encrypter/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -151,7 +66,7 @@ "it-stream-types": "^2.0.1" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-gater/package.json b/packages/interface-connection-gater/package.json index a048a32700..28ea0b4003 100644 --- a/packages/interface-connection-gater/package.json +++ b/packages/interface-connection-gater/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -129,10 +44,10 @@ "dependencies": { "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-peer-id": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-manager/package.json b/packages/interface-connection-manager/package.json index 1d405ebb65..1f9076c2f4 100644 --- a/packages/interface-connection-manager/package.json +++ b/packages/interface-connection-manager/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,11 +45,11 @@ "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/interfaces": "^3.0.0", - "@libp2p/peer-collections": "^3.0.1", - "@multiformats/multiaddr": "^12.0.0" + "@libp2p/peer-collections": "^3.0.0", + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-manager/tsconfig.json b/packages/interface-connection-manager/tsconfig.json index cbde3d05cb..3e21f519cc 100644 --- a/packages/interface-connection-manager/tsconfig.json +++ b/packages/interface-connection-manager/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../interfaces" + }, + { + "path": "../peer-collections" } ] } diff --git a/packages/interface-connection/package.json b/packages/interface-connection/package.json index 8fbfb60ba0..9bde521b06 100644 --- a/packages/interface-connection/package.json +++ b/packages/interface-connection/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -149,12 +64,12 @@ "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "it-stream-types": "^2.0.1", "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-content-routing/package.json b/packages/interface-content-routing/package.json index d73315f4de..e63a95f976 100644 --- a/packages/interface-content-routing/package.json +++ b/packages/interface-content-routing/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -129,10 +44,10 @@ "dependencies": { "@libp2p/interface-peer-info": "^1.0.0", "@libp2p/interfaces": "^3.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-dht/package.json b/packages/interface-dht/package.json index 69d381003e..46ac76cde9 100644 --- a/packages/interface-dht/package.json +++ b/packages/interface-dht/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,10 +46,10 @@ "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/interface-peer-info": "^1.0.0", "@libp2p/interfaces": "^3.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-keychain/package.json b/packages/interface-keychain/package.json index 1d786072db..8facba1250 100644 --- a/packages/interface-keychain/package.json +++ b/packages/interface-keychain/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -128,10 +43,10 @@ }, "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-keys/package.json b/packages/interface-keys/package.json index ff87fd33b5..8cc349bfe5 100644 --- a/packages/interface-keys/package.json +++ b/packages/interface-keys/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -127,7 +42,7 @@ "build": "aegir build" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-libp2p/package.json b/packages/interface-libp2p/package.json index 0057e4bd6b..1a97324de2 100644 --- a/packages/interface-libp2p/package.json +++ b/packages/interface-libp2p/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -138,10 +53,10 @@ "@libp2p/interface-registrar": "^2.0.0", "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-metrics/package.json b/packages/interface-metrics/package.json index 5e8774cacb..4d253595bd 100644 --- a/packages/interface-metrics/package.json +++ b/packages/interface-metrics/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,7 +45,7 @@ "@libp2p/interface-connection": "^5.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-mocks/package.json b/packages/interface-mocks/package.json index 0e2f4c096b..5a19d0d066 100644 --- a/packages/interface-mocks/package.json +++ b/packages/interface-mocks/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -148,12 +63,12 @@ "@libp2p/interface-stream-muxer": "^4.0.0", "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@libp2p/logger": "^2.1.1", - "@libp2p/multistream-select": "^3.1.8", - "@libp2p/peer-collections": "^3.0.1", + "@libp2p/logger": "^2.0.0", + "@libp2p/multistream-select": "^3.0.0", + "@libp2p/peer-collections": "^3.0.0", "@libp2p/peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", "it-handshake": "^4.1.3", @@ -165,14 +80,14 @@ "it-stream-types": "^2.0.1", "merge-options": "^3.0.4", "uint8arraylist": "^2.4.3", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "devDependencies": { "@libp2p/interface-connection-compliance-tests": "^2.0.0", "@libp2p/interface-connection-encrypter-compliance-tests": "^5.0.0", "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", "@libp2p/interface-stream-muxer-compliance-tests": "^7.0.0", - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-mocks/tsconfig.json b/packages/interface-mocks/tsconfig.json index d49aa5945a..19360077a1 100644 --- a/packages/interface-mocks/tsconfig.json +++ b/packages/interface-mocks/tsconfig.json @@ -61,6 +61,21 @@ }, { "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../multistream-select" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" } ] } diff --git a/packages/interface-peer-discovery-compliance-tests/package.json b/packages/interface-peer-discovery-compliance-tests/package.json index ceb756a488..5c541d6e09 100644 --- a/packages/interface-peer-discovery-compliance-tests/package.json +++ b/packages/interface-peer-discovery-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,7 +45,7 @@ "@libp2p/interface-compliance-tests": "^3.0.0", "@libp2p/interface-peer-discovery": "^2.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "aegir": "^39.0.5", "delay": "^6.0.0", "p-defer": "^4.0.0" diff --git a/packages/interface-peer-discovery/package.json b/packages/interface-peer-discovery/package.json index ca70962919..28f2e12c49 100644 --- a/packages/interface-peer-discovery/package.json +++ b/packages/interface-peer-discovery/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,7 +46,7 @@ "@libp2p/interfaces": "^3.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-id/package.json b/packages/interface-peer-id/package.json index bb29939d29..748a8c67c1 100644 --- a/packages/interface-peer-id/package.json +++ b/packages/interface-peer-id/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -127,10 +42,10 @@ "build": "aegir build" }, "dependencies": { - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-info/package.json b/packages/interface-peer-info/package.json index be47c49d99..fcdd888d93 100644 --- a/packages/interface-peer-info/package.json +++ b/packages/interface-peer-info/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -128,10 +43,10 @@ }, "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-routing/package.json b/packages/interface-peer-routing/package.json index cebeb953eb..5bb055b234 100644 --- a/packages/interface-peer-routing/package.json +++ b/packages/interface-peer-routing/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -132,7 +47,7 @@ "@libp2p/interfaces": "^3.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-store/package.json b/packages/interface-peer-store/package.json index 99c0d436c6..d548cfffee 100644 --- a/packages/interface-peer-store/package.json +++ b/packages/interface-peer-store/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -148,10 +63,10 @@ }, "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-pubsub-compliance-tests/package.json b/packages/interface-pubsub-compliance-tests/package.json index bf29959591..5fb951a1a3 100644 --- a/packages/interface-pubsub-compliance-tests/package.json +++ b/packages/interface-pubsub-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -138,10 +53,10 @@ "aegir": "^39.0.5", "delay": "^6.0.0", "p-defer": "^4.0.0", - "p-event": "^5.0.1", + "p-event": "^6.0.0", "p-wait-for": "^5.0.0", "sinon": "^15.0.0", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-pubsub-compliance-tests/tsconfig.json b/packages/interface-pubsub-compliance-tests/tsconfig.json index e128c8d1e9..1a1ed83a7b 100644 --- a/packages/interface-pubsub-compliance-tests/tsconfig.json +++ b/packages/interface-pubsub-compliance-tests/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../interfaces" + }, + { + "path": "../peer-id-factory" } ] } diff --git a/packages/interface-pubsub/package.json b/packages/interface-pubsub/package.json index 2e16c712cc..264136f0e5 100644 --- a/packages/interface-pubsub/package.json +++ b/packages/interface-pubsub/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -134,7 +49,7 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-record-compliance-tests/package.json b/packages/interface-record-compliance-tests/package.json index 83d8263247..0e73648f1b 100644 --- a/packages/interface-record-compliance-tests/package.json +++ b/packages/interface-record-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", diff --git a/packages/interface-record/package.json b/packages/interface-record/package.json index 9a31a1c865..7df588efd1 100644 --- a/packages/interface-record/package.json +++ b/packages/interface-record/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,7 +46,7 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-registrar/package.json b/packages/interface-registrar/package.json index 35b81c65c5..eda94ddd25 100644 --- a/packages/interface-registrar/package.json +++ b/packages/interface-registrar/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,7 +46,7 @@ "@libp2p/interface-peer-id": "^2.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-stream-muxer-compliance-tests/package.json b/packages/interface-stream-muxer-compliance-tests/package.json index 7a2c836cb4..ed4b33eac7 100644 --- a/packages/interface-stream-muxer-compliance-tests/package.json +++ b/packages/interface-stream-muxer-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -133,7 +48,7 @@ "abortable-iterator": "^5.0.1", "aegir": "^39.0.5", "delay": "^6.0.0", - "it-all": "^3.0.1", + "it-all": "^3.0.2", "it-drain": "^3.0.1", "it-map": "^3.0.2", "it-pair": "^2.0.2", @@ -142,7 +57,7 @@ "p-defer": "^4.0.0", "p-limit": "^4.0.0", "uint8arraylist": "^2.4.3", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-stream-muxer/package.json b/packages/interface-stream-muxer/package.json index 1f26ce0ba1..e238d958c2 100644 --- a/packages/interface-stream-muxer/package.json +++ b/packages/interface-stream-muxer/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -149,7 +64,7 @@ "dependencies": { "@libp2p/interface-connection": "^5.0.0", "@libp2p/interfaces": "^3.0.0", - "@libp2p/logger": "^2.1.1", + "@libp2p/logger": "^2.0.0", "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", "it-pushable": "^3.1.3", @@ -157,7 +72,7 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-stream-muxer/tsconfig.json b/packages/interface-stream-muxer/tsconfig.json index 181d553f61..30fd079a69 100644 --- a/packages/interface-stream-muxer/tsconfig.json +++ b/packages/interface-stream-muxer/tsconfig.json @@ -13,6 +13,9 @@ }, { "path": "../interfaces" + }, + { + "path": "../logger" } ] } diff --git a/packages/interface-transport-compliance-tests/package.json b/packages/interface-transport-compliance-tests/package.json index 20d73a23af..7e25d123d8 100644 --- a/packages/interface-transport-compliance-tests/package.json +++ b/packages/interface-transport-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -133,15 +48,15 @@ "@libp2p/interface-registrar": "^2.0.0", "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "aegir": "^39.0.5", - "it-all": "^3.0.1", + "it-all": "^3.0.2", "it-drain": "^3.0.1", "it-pipe": "^3.0.1", "p-defer": "^4.0.0", "p-wait-for": "^5.0.0", "sinon": "^15.0.0", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-transport/package.json b/packages/interface-transport/package.json index 5195f574c6..96e5e13b0f 100644 --- a/packages/interface-transport/package.json +++ b/packages/interface-transport/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,11 +45,11 @@ "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-stream-muxer": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "it-stream-types": "^2.0.1" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index 7f5c8e2f83..6f01645347 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -63,91 +63,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -155,7 +70,7 @@ "build": "aegir build" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/kad-dht/.aegir.js b/packages/kad-dht/.aegir.js new file mode 100644 index 0000000000..d0f4b4703f --- /dev/null +++ b/packages/kad-dht/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + bundlesizeMax: '160KB' + } +} diff --git a/packages/kad-dht/CHANGELOG.md b/packages/kad-dht/CHANGELOG.md new file mode 100644 index 0000000000..04d3b10bc3 --- /dev/null +++ b/packages/kad-dht/CHANGELOG.md @@ -0,0 +1,1654 @@ +## [9.3.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.5...v9.3.6) (2023-06-01) + + +### Bug Fixes + +* 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)) + +## [9.3.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.4...v9.3.5) (2023-05-26) + + +### Bug Fixes + +* use events to delay before self-query ([#478](https://github.com/libp2p/js-libp2p-kad-dht/issues/478)) ([46313a8](https://github.com/libp2p/js-libp2p-kad-dht/commit/46313a876783da7c036f79daa69a558bd8f1a245)) + + +### Dependencies + +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#476](https://github.com/libp2p/js-libp2p-kad-dht/issues/476)) ([50524f9](https://github.com/libp2p/js-libp2p-kad-dht/commit/50524f9d74a819fc1fbc2d6faff28a59e7d9b4aa)) + +## [9.3.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.3...v9.3.4) (2023-05-22) + + +### Bug Fixes + +* add events dep ([#477](https://github.com/libp2p/js-libp2p-kad-dht/issues/477)) ([3744a20](https://github.com/libp2p/js-libp2p-kad-dht/commit/3744a209e6bf8eaa75365056286f2f734c9ad1bf)) + +## [9.3.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.2...v9.3.3) (2023-05-09) + + +### Bug Fixes + +* work in browsers without extra deps ([#474](https://github.com/libp2p/js-libp2p-kad-dht/issues/474)) ([0c8d464](https://github.com/libp2p/js-libp2p-kad-dht/commit/0c8d464e91f63a799debd29c54dd358b38e11ea9)) + +## [9.3.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.1...v9.3.2) (2023-05-09) + + +### Bug Fixes + +* only start self-query if node is not stopped ([843fe61](https://github.com/libp2p/js-libp2p-kad-dht/commit/843fe6171b7bc8a61e0b5de18a7ccced5398307a)) + +## [9.3.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.0...v9.3.1) (2023-05-09) + + +### Bug Fixes + +* if client mode is specifed, do not auto-switch to server mode ([#475](https://github.com/libp2p/js-libp2p-kad-dht/issues/475)) ([abe6a25](https://github.com/libp2p/js-libp2p-kad-dht/commit/abe6a25c09b8e619661c427e5c7b7a4c45fa3d92)) + +## [9.3.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.2.0...v9.3.0) (2023-05-09) + + +### Features + +* switch to server mode automatically when addresses change ([#473](https://github.com/libp2p/js-libp2p-kad-dht/issues/473)) ([de51cbe](https://github.com/libp2p/js-libp2p-kad-dht/commit/de51cbe0c3f5e6b17c45d1297c359cdafd83b0a2)) + +## [9.2.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.5...v9.2.0) (2023-05-05) + + +### Features + +* invoke onProgress callback if passed as an option ([#472](https://github.com/libp2p/js-libp2p-kad-dht/issues/472)) ([0bef25f](https://github.com/libp2p/js-libp2p-kad-dht/commit/0bef25ff823581cf3462be32b23e737abd2fca3a)) + +## [9.1.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.4...v9.1.5) (2023-05-05) + + +### Bug Fixes + +* log peer id not whole object ([#470](https://github.com/libp2p/js-libp2p-kad-dht/issues/470)) ([e9efb7f](https://github.com/libp2p/js-libp2p-kad-dht/commit/e9efb7f7e5141bd40dc422cd45c964f8cc404764)) +* only choose query peers after initial self-query has run ([#471](https://github.com/libp2p/js-libp2p-kad-dht/issues/471)) ([4d05154](https://github.com/libp2p/js-libp2p-kad-dht/commit/4d0515497511e283c5d5b5a4d723c4ea783eb2a8)) + +## [9.1.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.3...v9.1.4) (2023-05-04) + + +### Dependencies + +* update @libp2p/interface-peer-discovery to 2.0.0 ([#469](https://github.com/libp2p/js-libp2p-kad-dht/issues/469)) ([2c0fc68](https://github.com/libp2p/js-libp2p-kad-dht/commit/2c0fc6865c4e6ddc5b598b1693cf8fb408a31ecf)) + +## [9.1.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.2...v9.1.3) (2023-05-04) + + +### Dependencies + +* **dev:** bump @libp2p/interface-libp2p from 2.0.0 to 3.0.0 ([#466](https://github.com/libp2p/js-libp2p-kad-dht/issues/466)) ([d8f8e5a](https://github.com/libp2p/js-libp2p-kad-dht/commit/d8f8e5a3b2d5c57fc89129c2cf8ed2ed7c52d171)) + +## [9.1.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.1...v9.1.2) (2023-05-04) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#468](https://github.com/libp2p/js-libp2p-kad-dht/issues/468)) ([dc53728](https://github.com/libp2p/js-libp2p-kad-dht/commit/dc53728db6772323bbac6e1e876e3d999f1d8fd0)) + +## [9.1.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.0...v9.1.1) (2023-05-04) + + +### Bug Fixes + +* wait for self-query to have run before running queries ([#457](https://github.com/libp2p/js-libp2p-kad-dht/issues/457)) ([9d5bdb9](https://github.com/libp2p/js-libp2p-kad-dht/commit/9d5bdb98e8ed9183064b895f9fc858bbde5f1127)) + +## [9.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.0.0...v9.1.0) (2023-04-27) + + +### Features + +* use symbols to return routers ([#464](https://github.com/libp2p/js-libp2p-kad-dht/issues/464)) ([d681aaf](https://github.com/libp2p/js-libp2p-kad-dht/commit/d681aaf49425e6e8638dc31eba5f158085cd4485)) + +## [9.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.12...v9.0.0) (2023-04-24) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 (#460) + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#460](https://github.com/libp2p/js-libp2p-kad-dht/issues/460)) ([e3f15f1](https://github.com/libp2p/js-libp2p-kad-dht/commit/e3f15f17a65d98940fcba4ba7a46fd17ab509785)) + +## [8.0.12](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.11...v8.0.12) (2023-04-19) + + +### Tests + +* enable skipped xor test ([#458](https://github.com/libp2p/js-libp2p-kad-dht/issues/458)) ([4acfd70](https://github.com/libp2p/js-libp2p-kad-dht/commit/4acfd7047154276ae70a2c1f07775abcacd82170)) + +## [8.0.11](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.10...v8.0.11) (2023-04-19) + + +### Dependencies + +* update it-stream-types ([#456](https://github.com/libp2p/js-libp2p-kad-dht/issues/456)) ([2dfccee](https://github.com/libp2p/js-libp2p-kad-dht/commit/2dfccee3614bbcde1bf0ff9b1c8dbded79f86d91)) + +## [8.0.10](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.9...v8.0.10) (2023-04-17) + + +### Bug Fixes + +* remove timeout-abort-controller dependency ([#454](https://github.com/libp2p/js-libp2p-kad-dht/issues/454)) ([7f3245e](https://github.com/libp2p/js-libp2p-kad-dht/commit/7f3245e5b9379c7fcf0eb3f7cccb60d2b81aaadb)) + +## [8.0.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.8...v8.0.9) (2023-04-14) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#452](https://github.com/libp2p/js-libp2p-kad-dht/issues/452)) ([75b1b50](https://github.com/libp2p/js-libp2p-kad-dht/commit/75b1b504ce529dfa447113092fd600041fb112de)) + +## [8.0.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.7...v8.0.8) (2023-04-14) + + +### Dependencies + +* bump @libp2p/interface-connection-manager from 1.5.0 to 2.0.0 ([#455](https://github.com/libp2p/js-libp2p-kad-dht/issues/455)) ([e4ed616](https://github.com/libp2p/js-libp2p-kad-dht/commit/e4ed6168add1653400853c5c2bc416790b0699a2)) + +## [8.0.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.6...v8.0.7) (2023-04-13) + + +### Dependencies + +* update any-signal to 4.x.x ([#453](https://github.com/libp2p/js-libp2p-kad-dht/issues/453)) ([852d757](https://github.com/libp2p/js-libp2p-kad-dht/commit/852d757cc09bfa468b7c405c3a1506d3e3cfb90b)) + +## [8.0.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.5...v8.0.6) (2023-04-03) + + +### Dependencies + +* update `it-*` deps to latest versions ([#450](https://github.com/libp2p/js-libp2p-kad-dht/issues/450)) ([0d07558](https://github.com/libp2p/js-libp2p-kad-dht/commit/0d07558c94728f2a0323ccd6d86b3816f4562966)) + +## [8.0.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.4...v8.0.5) (2023-03-31) + + +### Dependencies + +* bump it-map from 2.0.1 to 3.0.1 ([#440](https://github.com/libp2p/js-libp2p-kad-dht/issues/440)) ([8e02b3d](https://github.com/libp2p/js-libp2p-kad-dht/commit/8e02b3d6db1f323e103fbcdff59fabcc8f4d67c6)) +* bump it-take from 2.0.1 to 3.0.1 ([#439](https://github.com/libp2p/js-libp2p-kad-dht/issues/439)) ([f85e2f9](https://github.com/libp2p/js-libp2p-kad-dht/commit/f85e2f9576095035117ddb3f5794bf9aef1e8453)) +* **dev:** bump it-last from 2.0.1 to 3.0.1 ([#438](https://github.com/libp2p/js-libp2p-kad-dht/issues/438)) ([23cc94f](https://github.com/libp2p/js-libp2p-kad-dht/commit/23cc94f224dbb01a3f0454c6a8d9f6722a1f40c4)) + +## [8.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.3...v8.0.4) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#436](https://github.com/libp2p/js-libp2p-kad-dht/issues/436)) ([5024646](https://github.com/libp2p/js-libp2p-kad-dht/commit/502464690ab8f5610fea9347fc99f2a41cf62774)) + +## [8.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.2...v8.0.3) (2023-03-29) + + +### Bug Fixes + +* updated for pqueue7 ([#433](https://github.com/libp2p/js-libp2p-kad-dht/issues/433)) ([62205a0](https://github.com/libp2p/js-libp2p-kad-dht/commit/62205a0cce3f40f238116810f75640466f8e3972)) + +## [8.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.1...v8.0.2) (2023-03-24) + + +### Dependencies + +* **dev:** bump @types/which from 2.0.2 to 3.0.0 ([#435](https://github.com/libp2p/js-libp2p-kad-dht/issues/435)) ([f8ad02a](https://github.com/libp2p/js-libp2p-kad-dht/commit/f8ad02a26d80a66f684817e2587e5f705e83de2b)) + +## [8.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.0...v8.0.1) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#434](https://github.com/libp2p/js-libp2p-kad-dht/issues/434)) ([49dcc65](https://github.com/libp2p/js-libp2p-kad-dht/commit/49dcc65c4b34bc4ddce8f04f6b1a9761f1693311)) + +## [8.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.3...v8.0.0) (2023-03-14) + + +### ⚠ BREAKING CHANGES + +* requires an instance of `interface-datastore@8.x.x` + +### Dependencies + +* update interface-datastore to 8.x.x ([#430](https://github.com/libp2p/js-libp2p-kad-dht/issues/430)) ([923ef72](https://github.com/libp2p/js-libp2p-kad-dht/commit/923ef72ff39930fb773f3d619f8d1a23cf15d65d)) + +## [7.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.2...v7.0.3) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#416](https://github.com/libp2p/js-libp2p-kad-dht/issues/416)) ([7ebf172](https://github.com/libp2p/js-libp2p-kad-dht/commit/7ebf172b6b454315ff8d0fcd15509b768a5da4ad)) +* **dev:** bump execa from 6.1.0 to 7.0.0 ([#421](https://github.com/libp2p/js-libp2p-kad-dht/issues/421)) ([04124d4](https://github.com/libp2p/js-libp2p-kad-dht/commit/04124d48a065d226a4ec370efec38dfe4f3c5ff4)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.1...v7.0.2) (2023-03-10) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#427](https://github.com/libp2p/js-libp2p-kad-dht/issues/427)) ([bf7d1ba](https://github.com/libp2p/js-libp2p-kad-dht/commit/bf7d1bac21d0346098cc206951602dc39224d43a)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.0...v7.0.1) (2023-03-10) + + +### Bug Fixes + +* correct `KBucketTree` types ([#426](https://github.com/libp2p/js-libp2p-kad-dht/issues/426)) ([ea8e6d0](https://github.com/libp2p/js-libp2p-kad-dht/commit/ea8e6d0fc7db9192539532c8d74b3e5e053056fd)), closes [/github.com/tristanls/k-bucket/blob/3aa5b4f1dacb835752995a25409ab319d2070b9e/index.js#L413](https://github.com/libp2p//github.com/tristanls/k-bucket/blob/3aa5b4f1dacb835752995a25409ab319d2070b9e/index.js/issues/L413) +* update p-queue types ([#428](https://github.com/libp2p/js-libp2p-kad-dht/issues/428)) ([f5b85fc](https://github.com/libp2p/js-libp2p-kad-dht/commit/f5b85fccfd920984073319f6c62015231611ba26)) + + +### Trivial Changes + +* replace err-code with CodeError ([#413](https://github.com/libp2p/js-libp2p-kad-dht/issues/413)) ([e05d2a0](https://github.com/libp2p/js-libp2p-kad-dht/commit/e05d2a07eee96bfd91bdd01707f5c8112151c377)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([a70ab3f](https://github.com/libp2p/js-libp2p-kad-dht/commit/a70ab3f200cb73703b6301b81ba6b922c37b5bc3)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1652c6c](https://github.com/libp2p/js-libp2p-kad-dht/commit/1652c6cccd8dc1381c4b28091f87cadcac9782b5)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([ea13c2a](https://github.com/libp2p/js-libp2p-kad-dht/commit/ea13c2a10c689655656880b584315ebab374871c)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.1.1...v7.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 (#412) + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#412](https://github.com/libp2p/js-libp2p-kad-dht/issues/412)) ([18c8276](https://github.com/libp2p/js-libp2p-kad-dht/commit/18c8276b8638c2e3a3733dae2c973c552f92303c)) + +## [6.1.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.1.0...v6.1.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#411](https://github.com/libp2p/js-libp2p-kad-dht/issues/411)) ([718e01b](https://github.com/libp2p/js-libp2p-kad-dht/commit/718e01b86299e69ac1f2fed12c77b0675aeee9cf)) + +## [6.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.4...v6.1.0) (2022-12-07) + + +### Features + +* allow passing ProvidersInit in KadDHT constructor ([#404](https://github.com/libp2p/js-libp2p-kad-dht/issues/404)) ([e64af85](https://github.com/libp2p/js-libp2p-kad-dht/commit/e64af85d6ef02d99521689ed8b60e0c3702efbc5)) + + +### Bug Fixes + +* treat /dns, /dns4, and /dns6 addrs as public ([#406](https://github.com/libp2p/js-libp2p-kad-dht/issues/406)) ([e27747a](https://github.com/libp2p/js-libp2p-kad-dht/commit/e27747ab9c32b6f72b04bb24cbc51e95384c1747)), closes [#377](https://github.com/libp2p/js-libp2p-kad-dht/issues/377) + +## [6.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.3...v6.0.4) (2022-12-07) + + +### Bug Fixes + +* use multihash bytes for provide message keys ([#405](https://github.com/libp2p/js-libp2p-kad-dht/issues/405)) ([d7e7b5d](https://github.com/libp2p/js-libp2p-kad-dht/commit/d7e7b5d56334ca6e9a305c9473e9d62468933f05)), closes [#381](https://github.com/libp2p/js-libp2p-kad-dht/issues/381) + +## [6.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.2...v6.0.3) (2022-12-07) + + +### Dependencies + +* bump private-ip from 2.3.4 to 3.0.0 ([#400](https://github.com/libp2p/js-libp2p-kad-dht/issues/400)) ([5a567e3](https://github.com/libp2p/js-libp2p-kad-dht/commit/5a567e31e46671c0e5bcb9a7859a2ca82e33e58a)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.1...v6.0.2) (2022-12-07) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 7.1.0 to 8.0.1 ([#399](https://github.com/libp2p/js-libp2p-kad-dht/issues/399)) ([26cbb88](https://github.com/libp2p/js-libp2p-kad-dht/commit/26cbb88772da0496b46909019ea4b371a7947b51)) +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#403](https://github.com/libp2p/js-libp2p-kad-dht/issues/403)) ([f78a0ea](https://github.com/libp2p/js-libp2p-kad-dht/commit/f78a0ea8d0f387906422797668697fb66d4b3749)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.0...v6.0.1) (2022-11-07) + + +### Bug Fixes + +* enable and fix browser tests ([#352](https://github.com/libp2p/js-libp2p-kad-dht/issues/352)) ([5244428](https://github.com/libp2p/js-libp2p-kad-dht/commit/5244428d0a6a7c3151f2fcb043400e61a4c78a36)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v5.0.2...v6.0.0) (2022-11-05) + + +### ⚠ BREAKING CHANGES + +* requires @libp2p/interface-metrics v4 + +### Bug Fixes + +* update to metrics v4 ([#398](https://github.com/libp2p/js-libp2p-kad-dht/issues/398)) ([3182cb0](https://github.com/libp2p/js-libp2p-kad-dht/commit/3182cb0dbcfa8eca0ceb243db53c43eed14c1af8)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v5.0.1...v5.0.2) (2022-11-05) + + +### Dependencies + +* bump it-all from 1.0.6 to 2.0.0 ([#389](https://github.com/libp2p/js-libp2p-kad-dht/issues/389)) ([0596485](https://github.com/libp2p/js-libp2p-kad-dht/commit/059648530a991cb04e8f7408791d6facf4b57e41)) +* bump it-drain from 1.0.5 to 2.0.0 ([#390](https://github.com/libp2p/js-libp2p-kad-dht/issues/390)) ([fdda357](https://github.com/libp2p/js-libp2p-kad-dht/commit/fdda357118f7020469aba57122f322e6210ff907)) +* bump it-first from 1.0.7 to 2.0.0 ([#391](https://github.com/libp2p/js-libp2p-kad-dht/issues/391)) ([681c24e](https://github.com/libp2p/js-libp2p-kad-dht/commit/681c24ea1cd7c65f82ca76c00c9c3051432665f8)) +* bump it-length from 1.0.4 to 2.0.0 ([#394](https://github.com/libp2p/js-libp2p-kad-dht/issues/394)) ([ae07736](https://github.com/libp2p/js-libp2p-kad-dht/commit/ae0773619c80c30c2eb556a8802833c1051c3cb0)) +* bump it-map from 1.0.6 to 2.0.0 ([#396](https://github.com/libp2p/js-libp2p-kad-dht/issues/396)) ([ac5101c](https://github.com/libp2p/js-libp2p-kad-dht/commit/ac5101c764a7eb1be0d531e6a61268b2a8f1a613)) +* bump it-merge from 1.0.4 to 2.0.0 ([#393](https://github.com/libp2p/js-libp2p-kad-dht/issues/393)) ([1acb5f1](https://github.com/libp2p/js-libp2p-kad-dht/commit/1acb5f170351a72860bf9552ecba0f1bf98dc3b6)) +* bump it-parallel from 2.0.2 to 3.0.0 ([#392](https://github.com/libp2p/js-libp2p-kad-dht/issues/392)) ([06a2c48](https://github.com/libp2p/js-libp2p-kad-dht/commit/06a2c480f88447c8631d9348ad28af5308d73eb9)) +* bump it-take from 1.0.2 to 2.0.0 ([#397](https://github.com/libp2p/js-libp2p-kad-dht/issues/397)) ([4e909d2](https://github.com/libp2p/js-libp2p-kad-dht/commit/4e909d286e0bee6f423abe59216b412fe38a2563)) +* **dev:** bump it-filter from 1.0.3 to 2.0.0 ([#395](https://github.com/libp2p/js-libp2p-kad-dht/issues/395)) ([5668f9c](https://github.com/libp2p/js-libp2p-kad-dht/commit/5668f9c6ea9afddc76dd41714515cea65aed1c50)) +* **dev:** bump it-last from 1.0.6 to 2.0.0 ([#388](https://github.com/libp2p/js-libp2p-kad-dht/issues/388)) ([5b55239](https://github.com/libp2p/js-libp2p-kad-dht/commit/5b55239d5a85cb00ea8189fa2803a881313070cc)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v5.0.0...v5.0.1) (2022-10-12) + + +### Bug Fixes + +* update export ([#387](https://github.com/libp2p/js-libp2p-kad-dht/issues/387)) ([9dbbe55](https://github.com/libp2p/js-libp2p-kad-dht/commit/9dbbe55a6a525e78ed8ee8cc7b30636ff93cd18d)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v4.0.2...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#386](https://github.com/libp2p/js-libp2p-kad-dht/issues/386)) ([abe5207](https://github.com/libp2p/js-libp2p-kad-dht/commit/abe52072b41d4188af59d61a78b02f38f1cc38a8)), closes [libp2p/js-libp2p-components#6](https://github.com/libp2p/js-libp2p-components/issues/6) + +## [4.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v4.0.1...v4.0.2) (2022-10-11) + + +### Dependencies + +* **dev:** bump @libp2p/peer-store from 3.1.5 to 4.0.0 ([#382](https://github.com/libp2p/js-libp2p-kad-dht/issues/382)) ([94c0dc8](https://github.com/libp2p/js-libp2p-kad-dht/commit/94c0dc850f2b7f16eaa6e70d7157c3b2a3bbf7d5)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v4.0.0...v4.0.1) (2022-10-11) + + +### Dependencies + +* bump @libp2p/interface-address-manager from 1.0.3 to 2.0.0 ([#383](https://github.com/libp2p/js-libp2p-kad-dht/issues/383)) ([5e7dfeb](https://github.com/libp2p/js-libp2p-kad-dht/commit/5e7dfeb0fa0479180d92e604052dbb5b16baf5f7)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.1.0...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#379) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#379](https://github.com/libp2p/js-libp2p-kad-dht/issues/379)) ([124be9c](https://github.com/libp2p/js-libp2p-kad-dht/commit/124be9c3be6d49a460d7fdb3e01217fcd7729e8d)) +* **dev:** bump @libp2p/interface-mocks from 4.0.3 to 6.0.0 ([#378](https://github.com/libp2p/js-libp2p-kad-dht/issues/378)) ([fc6741b](https://github.com/libp2p/js-libp2p-kad-dht/commit/fc6741b01edf5c54dfa8177aff20e035bd28f425)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.6...v3.1.0) (2022-10-04) + + +### Features + +* tag kad-close peers ([#375](https://github.com/libp2p/js-libp2p-kad-dht/issues/375)) ([df15a83](https://github.com/libp2p/js-libp2p-kad-dht/commit/df15a832f9f274b3ebea9b7752e66a149828147c)) + +## [3.0.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.5...v3.0.6) (2022-09-29) + + +### Bug Fixes + +* re-enable ensuring queries run along disjoint paths ([#371](https://github.com/libp2p/js-libp2p-kad-dht/issues/371)) ([5ae4440](https://github.com/libp2p/js-libp2p-kad-dht/commit/5ae4440e7578d3d6adb557f780fcd23ce7fc13b5)) + + +### Trivial Changes + +* remove IRC badge from readme ([#374](https://github.com/libp2p/js-libp2p-kad-dht/issues/374)) ([e48754f](https://github.com/libp2p/js-libp2p-kad-dht/commit/e48754fa5f16420000804d5f8cb9f536cb7e2596)) + +## [3.0.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.4...v3.0.5) (2022-09-21) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#372](https://github.com/libp2p/js-libp2p-kad-dht/issues/372)) ([cb0bc9e](https://github.com/libp2p/js-libp2p-kad-dht/commit/cb0bc9e2ec44a7d9ba9ac3c2b6079af5b6d76840)) + + +### Trivial Changes + +* increase test timeouts ([#373](https://github.com/libp2p/js-libp2p-kad-dht/issues/373)) ([ad25fae](https://github.com/libp2p/js-libp2p-kad-dht/commit/ad25fae47426fd14ac14364e83b755837fb3a400)) +* Update .github/workflows/stale.yml [skip ci] ([2236c50](https://github.com/libp2p/js-libp2p-kad-dht/commit/2236c505f756c3efa7deefe70a2c070f009c36c6)) + +## [3.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.3...v3.0.4) (2022-08-12) + + +### Dependencies + +* update interface-datastore and datastore-level ([#367](https://github.com/libp2p/js-libp2p-kad-dht/issues/367)) ([b2f8e55](https://github.com/libp2p/js-libp2p-kad-dht/commit/b2f8e557eb9721873a401f59ddd3fa12d6ee2ba3)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.2...v3.0.3) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#364](https://github.com/libp2p/js-libp2p-kad-dht/issues/364)) ([52323ab](https://github.com/libp2p/js-libp2p-kad-dht/commit/52323ab051bc6a02084c5653d9998aaa5721b49f)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.1...v3.0.2) (2022-08-10) + + +### Bug Fixes + +* update deps to new versions ([#363](https://github.com/libp2p/js-libp2p-kad-dht/issues/363)) ([7d058d6](https://github.com/libp2p/js-libp2p-kad-dht/commit/7d058d6b8dfd28fd83991778bc32ff463238fe42)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.0...v3.0.1) (2022-07-31) + + +### Trivial Changes + +* update project config ([#356](https://github.com/libp2p/js-libp2p-kad-dht/issues/356)) ([d944d81](https://github.com/libp2p/js-libp2p-kad-dht/commit/d944d81a24875aad602b2a56bdd6a2c012e4208e)) + + +### Dependencies + +* update it-length-prefixed and protons for no-copy ops ([#357](https://github.com/libp2p/js-libp2p-kad-dht/issues/357)) ([518abfe](https://github.com/libp2p/js-libp2p-kad-dht/commit/518abfe34d7b733f7652ea72e6302a15fc5de64b)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v2.0.0...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* Updates to new registrar API + +### Features + +* specify stream limits ([#348](https://github.com/libp2p/js-libp2p-kad-dht/issues/348)) ([c2a9238](https://github.com/libp2p/js-libp2p-kad-dht/commit/c2a923863c2b4dd1fd95922ffdb21b6bc45f42c8)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.16...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update libp2p interfaces ([#345](https://github.com/libp2p/js-libp2p-kad-dht/issues/345)) ([273f756](https://github.com/libp2p/js-libp2p-kad-dht/commit/273f756fe9da43e9eb6184f51eae29d6d50cffac)) + +### [1.0.16](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.15...v1.0.16) (2022-06-01) + + +### Bug Fixes + +* ping peers with correct protocol ([#344](https://github.com/libp2p/js-libp2p-kad-dht/issues/344)) ([e7ccf13](https://github.com/libp2p/js-libp2p-kad-dht/commit/e7ccf13df07c6a4770fb75307996fd38bfbb54ae)) + +### [1.0.15](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.14...v1.0.15) (2022-05-25) + + +### Trivial Changes + +* fix readme.md ([#342](https://github.com/libp2p/js-libp2p-kad-dht/issues/342)) ([ddea70d](https://github.com/libp2p/js-libp2p-kad-dht/commit/ddea70d23faca82b269271be3fa647896cf498e7)), closes [#324](https://github.com/libp2p/js-libp2p-kad-dht/issues/324) + +### [1.0.14](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.13...v1.0.14) (2022-05-24) + + +### Bug Fixes + +* increase ping concurrency ([#341](https://github.com/libp2p/js-libp2p-kad-dht/issues/341)) ([fa7cfc1](https://github.com/libp2p/js-libp2p-kad-dht/commit/fa7cfc15134cd3909f1c12ea222bae3d1cb06360)) + +### [1.0.13](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.12...v1.0.13) (2022-05-23) + + +### Bug Fixes + +* update deps ([#340](https://github.com/libp2p/js-libp2p-kad-dht/issues/340)) ([69ebfbd](https://github.com/libp2p/js-libp2p-kad-dht/commit/69ebfbd2aaa4d0fb948d2f8c0c8b329b2222aee9)) + +### [1.0.12](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.11...v1.0.12) (2022-05-16) + + +### Bug Fixes + +* remove p-map dependency ([#335](https://github.com/libp2p/js-libp2p-kad-dht/issues/335)) ([b50039d](https://github.com/libp2p/js-libp2p-kad-dht/commit/b50039d1e0e7fba6ed5c8cc25777302037a0dadb)) + +### [1.0.11](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.10...v1.0.11) (2022-05-10) + + +### Bug Fixes + +* encode enums correctly ([#332](https://github.com/libp2p/js-libp2p-kad-dht/issues/332)) ([af1e701](https://github.com/libp2p/js-libp2p-kad-dht/commit/af1e70179bf2889015966a48de38450289920ae1)) + +### [1.0.10](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.9...v1.0.10) (2022-05-06) + + +### Bug Fixes + +* update interfaces ([#330](https://github.com/libp2p/js-libp2p-kad-dht/issues/330)) ([e10d5f5](https://github.com/libp2p/js-libp2p-kad-dht/commit/e10d5f5346adbfef5f46fa81961f8870b48210f9)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.8...v1.0.9) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#329](https://github.com/libp2p/js-libp2p-kad-dht/issues/329)) ([b97187e](https://github.com/libp2p/js-libp2p-kad-dht/commit/b97187ed26ec49ee8cc551f2f1e1e72dcdd9de13)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.7...v1.0.8) (2022-05-03) + + +### Trivial Changes + +* update aegir config ([#312](https://github.com/libp2p/js-libp2p-kad-dht/issues/312)) ([526e65e](https://github.com/libp2p/js-libp2p-kad-dht/commit/526e65ede9bc154245970bda3f63113a81ca6c6e)) +* update interfaces to new version ([#327](https://github.com/libp2p/js-libp2p-kad-dht/issues/327)) ([388042b](https://github.com/libp2p/js-libp2p-kad-dht/commit/388042b239e7dc9215c4f0e0cd9d2424df4109f6)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.6...v1.0.7) (2022-04-19) + + +### Bug Fixes + +* return self when asked for self ([#318](https://github.com/libp2p/js-libp2p-kad-dht/issues/318)) ([c84fa2a](https://github.com/libp2p/js-libp2p-kad-dht/commit/c84fa2a7959e9c1b2f60349b7d9271df7cb992d0)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.5...v1.0.6) (2022-04-17) + + +### Bug Fixes + +* max listeners warning ([#316](https://github.com/libp2p/js-libp2p-kad-dht/issues/316)) ([18ba9c0](https://github.com/libp2p/js-libp2p-kad-dht/commit/18ba9c0b57e6aa51cbbaeea44b36888be88b1df7)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.4...v1.0.5) (2022-04-14) + + +### Trivial Changes + +* update deps ([#313](https://github.com/libp2p/js-libp2p-kad-dht/issues/313)) ([347a597](https://github.com/libp2p/js-libp2p-kad-dht/commit/347a5974edc34fb548305929bdc3af10da2a0563)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.3...v1.0.4) (2022-04-09) + + +### Trivial Changes + +* update aegir ([#311](https://github.com/libp2p/js-libp2p-kad-dht/issues/311)) ([fc44105](https://github.com/libp2p/js-libp2p-kad-dht/commit/fc44105e6c2c1ea6714201e5be26fbd7c8b10321)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.2...v1.0.3) (2022-03-24) + + +### Bug Fixes + +* update interfaces ([#305](https://github.com/libp2p/js-libp2p-kad-dht/issues/305)) ([2def2bd](https://github.com/libp2p/js-libp2p-kad-dht/commit/2def2bdcd2cf60daeb36da94c041361cc2e683cb)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.1...v1.0.2) (2022-03-23) + + +### Bug Fixes + +* add record validators and selectors ([#304](https://github.com/libp2p/js-libp2p-kad-dht/issues/304)) ([27c3948](https://github.com/libp2p/js-libp2p-kad-dht/commit/27c3948376106ba8bc80e5ac0148e64f5b066cb0)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.0...v1.0.1) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#302](https://github.com/libp2p/js-libp2p-kad-dht/issues/302)) ([940aba3](https://github.com/libp2p/js-libp2p-kad-dht/commit/940aba35018e5b34cf43d682e29176a1df37d20b)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.6...v1.0.0) (2022-03-04) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#300](https://github.com/libp2p/js-libp2p-kad-dht/issues/300)) ([9696346](https://github.com/libp2p/js-libp2p-kad-dht/commit/9696346bcf48e882c0126268bdec99ec01ec2023)) + +## [0.28.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.5...v0.28.6) (2022-01-19) + + + +## [0.28.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.4...v0.28.5) (2022-01-19) + + +### Bug Fixes + +* update component metric API use ([#293](https://github.com/libp2p/js-libp2p-kad-dht/issues/293)) ([c026f03](https://github.com/libp2p/js-libp2p-kad-dht/commit/c026f0389373718131ee26424b786e55285c1c5e)) + + + +## [0.28.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.3...v0.28.4) (2022-01-17) + + + +## [0.28.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.2...v0.28.3) (2022-01-17) + + +### Bug Fixes + +* catch not found errors ([#291](https://github.com/libp2p/js-libp2p-kad-dht/issues/291)) ([f0a4307](https://github.com/libp2p/js-libp2p-kad-dht/commit/f0a430731d5b026d80495ec1c8fc457d77c29451)) + + + +## [0.28.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.1...v0.28.2) (2022-01-15) + + +### Bug Fixes + +* remove abort controller deps ([#276](https://github.com/libp2p/js-libp2p-kad-dht/issues/276)) ([26cd857](https://github.com/libp2p/js-libp2p-kad-dht/commit/26cd8571a0e050f4ef524f2672d604dcd1288b14)) + + + +## [0.28.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.0...v0.28.1) (2021-12-31) + + +### Bug Fixes + +* catch errors from setMaxListeners ([#275](https://github.com/libp2p/js-libp2p-kad-dht/issues/275)) ([de2c601](https://github.com/libp2p/js-libp2p-kad-dht/commit/de2c601632bac41cc8b85b2d3a122f4ed24a7aed)) + + + +# [0.28.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.6...v0.28.0) (2021-12-30) + + +### Features + +* async peer store ([#272](https://github.com/libp2p/js-libp2p-kad-dht/issues/272)) ([12804e2](https://github.com/libp2p/js-libp2p-kad-dht/commit/12804e260e76ac9b990244ff437e9147795fde3d)) + + +### BREAKING CHANGES + +* peerstore methods are now all async + + + +## [0.27.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.5...v0.27.6) (2021-12-29) + + +### Bug Fixes + +* return pk when found ([#273](https://github.com/libp2p/js-libp2p-kad-dht/issues/273)) ([e7d2d7f](https://github.com/libp2p/js-libp2p-kad-dht/commit/e7d2d7ff6744fda4d984bf1ca802027427726809)) + + + +## [0.27.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.4...v0.27.5) (2021-12-21) + + +### Bug Fixes + +* silence max listeners exceeded warning ([#270](https://github.com/libp2p/js-libp2p-kad-dht/issues/270)) ([7b6c90f](https://github.com/libp2p/js-libp2p-kad-dht/commit/7b6c90fa76207a028c14609b4f8834ae9be2bf76)) + + + +## [0.27.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.3...v0.27.4) (2021-12-15) + + +### Features + +* log component metrics ([#269](https://github.com/libp2p/js-libp2p-kad-dht/issues/269)) ([db4f7f7](https://github.com/libp2p/js-libp2p-kad-dht/commit/db4f7f7e56ff7f146112c06f18ffe93f359b8856)) + + + +## [0.27.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.2...v0.27.3) (2021-12-07) + + +### Bug Fixes + +* add default query timeouts ([#266](https://github.com/libp2p/js-libp2p-kad-dht/issues/266)) ([4df2c3f](https://github.com/libp2p/js-libp2p-kad-dht/commit/4df2c3f1f1a8de7583e71acecb64a03e050263d4)) + + + +## [0.27.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.1...v0.27.2) (2021-12-03) + + +### Bug Fixes + +* only provide to wan in server mode ([#264](https://github.com/libp2p/js-libp2p-kad-dht/issues/264)) ([79c0bdb](https://github.com/libp2p/js-libp2p-kad-dht/commit/79c0bdb6471adbea69383f3537c73cf8c5797de8)) + + + +## [0.27.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.0...v0.27.1) (2021-12-02) + + + +# [0.27.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.7...v0.27.0) (2021-12-01) + + +### Bug Fixes + +* do not send messages if the network is not running ([#259](https://github.com/libp2p/js-libp2p-kad-dht/issues/259)) ([50ea7aa](https://github.com/libp2p/js-libp2p-kad-dht/commit/50ea7aaa5b22fc7269ec73bf269abfcf6f35b657)) + + +### chore + +* update libp2p-crypto ([#260](https://github.com/libp2p/js-libp2p-kad-dht/issues/260)) ([64f775b](https://github.com/libp2p/js-libp2p-kad-dht/commit/64f775b34397d02eec6eb3c2ccde05abab551722)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +## [0.26.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.6...v0.26.7) (2021-11-26) + + + +## [0.26.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.5...v0.26.6) (2021-11-26) + + +### Bug Fixes + +* increase time between table refresh ([#256](https://github.com/libp2p/js-libp2p-kad-dht/issues/256)) ([1471fb9](https://github.com/libp2p/js-libp2p-kad-dht/commit/1471fb94000c6f80c8a7d64b4a9ca342275a7ec8)) + + + +## [0.26.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.4...v0.26.5) (2021-11-25) + + +### Bug Fixes + +* do not pollute routing table with useless peers ([#254](https://github.com/libp2p/js-libp2p-kad-dht/issues/254)) ([4f79899](https://github.com/libp2p/js-libp2p-kad-dht/commit/4f7989900c6239fa449841be90ebe7f0ed517316)) + + + +## [0.26.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.3...v0.26.4) (2021-11-25) + + +### Bug Fixes + +* require at least one successful put ([#253](https://github.com/libp2p/js-libp2p-kad-dht/issues/253)) ([f7a2a02](https://github.com/libp2p/js-libp2p-kad-dht/commit/f7a2a02ef49c35bf7de1fc0d7a8256281819a740)) + + + +## [0.26.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.2...v0.26.3) (2021-11-25) + + +### Bug Fixes + +* count successful puts ([#252](https://github.com/libp2p/js-libp2p-kad-dht/issues/252)) ([d90f1a6](https://github.com/libp2p/js-libp2p-kad-dht/commit/d90f1a61d8bfd1128312ab5daed3bf831aced74d)) + + + +## [0.26.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.1...v0.26.2) (2021-11-24) + + +### Bug Fixes + +* remove trailing slash from datastore prefixes ([#241](https://github.com/libp2p/js-libp2p-kad-dht/issues/241)) ([2d26f9b](https://github.com/libp2p/js-libp2p-kad-dht/commit/2d26f9bbf77ceabf6cdc7904896454b82b2a8b38)) + + + +## [0.26.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.0...v0.26.1) (2021-11-22) + + +### Bug Fixes + +* prefix records with key, remove disjoint queries ([#239](https://github.com/libp2p/js-libp2p-kad-dht/issues/239)) ([e31696c](https://github.com/libp2p/js-libp2p-kad-dht/commit/e31696c3a4363f2fa7c6a6534d67b57252bafe36)) + + + +# [0.26.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.25.0...v0.26.0) (2021-11-18) + + +### Bug Fixes + +* refactor query logic ([#237](https://github.com/libp2p/js-libp2p-kad-dht/issues/237)) ([1f8bc6a](https://github.com/libp2p/js-libp2p-kad-dht/commit/1f8bc6a23d3db592606c789648f13199078e176c)) + + +### Features + +* ping old DHT peers before eviction ([#229](https://github.com/libp2p/js-libp2p-kad-dht/issues/229)) ([eff54bf](https://github.com/libp2p/js-libp2p-kad-dht/commit/eff54bf0c40f03dcff03d139d0bb275e2af175b0)) + + + +# [0.25.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.24.0...v0.25.0) (2021-09-24) + + +### Bug Fixes + +* browser compatibility ([#226](https://github.com/libp2p/js-libp2p-kad-dht/issues/226)) ([01b7ec1](https://github.com/libp2p/js-libp2p-kad-dht/commit/01b7ec15c059653a83634020bc9668bd7d25c1a9)) +* browser override path ([#228](https://github.com/libp2p/js-libp2p-kad-dht/issues/228)) ([3c737c1](https://github.com/libp2p/js-libp2p-kad-dht/commit/3c737c16399ac7e541b417f0e8b76157ed2f86ff)) + + +### chore + +* update datastore ([#227](https://github.com/libp2p/js-libp2p-kad-dht/issues/227)) ([64a3044](https://github.com/libp2p/js-libp2p-kad-dht/commit/64a304432ecc69c5a13b2af17781ea8b833295d0)) + + +### BREAKING CHANGES + +* provided datastore must implement interface-datastore@6.0.0 + + + +## [0.24.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.24.1...v0.24.2) (2021-09-14) + + +### Bug Fixes + +* browser override path ([#228](https://github.com/libp2p/js-libp2p-kad-dht/issues/228)) ([3c737c1](https://github.com/libp2p/js-libp2p-kad-dht/commit/3c737c16399ac7e541b417f0e8b76157ed2f86ff)) + + + +## [0.24.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.24.0...v0.24.1) (2021-09-07) + + +### Bug Fixes + +* browser compatibility ([#226](https://github.com/libp2p/js-libp2p-kad-dht/issues/226)) ([01b7ec1](https://github.com/libp2p/js-libp2p-kad-dht/commit/01b7ec15c059653a83634020bc9668bd7d25c1a9)) + + + +# [0.24.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.4...v0.24.0) (2021-09-03) + + +### Features + +* periodically fill the routing table with KADIds ([#215](https://github.com/libp2p/js-libp2p-kad-dht/issues/215)) ([d812a91](https://github.com/libp2p/js-libp2p-kad-dht/commit/d812a91e7b59589e8f46b60ba23dcbb4db02d75a)) + + +### BREAKING CHANGES + +* .start() is now async and random walk has been removed + + + +## [0.23.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.3...v0.23.4) (2021-09-03) + + +### Reverts + +* Revert "feat: periodically fill the routing table with KADIds (#215)" ([dd16a28](https://github.com/libp2p/js-libp2p-kad-dht/commit/dd16a28d321e82b8c41ca942a07023b31c23f250)), closes [#215](https://github.com/libp2p/js-libp2p-kad-dht/issues/215) + + + +## [0.23.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.2...v0.23.3) (2021-09-03) + + +### Features + +* periodically fill the routing table with KADIds ([#215](https://github.com/libp2p/js-libp2p-kad-dht/issues/215)) ([10f0cc8](https://github.com/libp2p/js-libp2p-kad-dht/commit/10f0cc860b47581019dd8d9ec5b383337708679d)) + + +### BREAKING CHANGES + +* .start() is now async and random walk has been removed + + + +## [0.23.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.22.0...v0.23.2) (2021-08-18) + + +### chore + +* update to new multiformats ([#220](https://github.com/libp2p/js-libp2p-kad-dht/issues/220)) ([565eb00](https://github.com/libp2p/js-libp2p-kad-dht/commit/565eb003c0c5d165088d113f8caecc5f7a5a12ad)) + + +### BREAKING CHANGES + +* uses new multiformats CID class + + + +## [0.23.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.0...v0.23.1) (2021-07-08) + + + +# [0.23.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.22.0...v0.23.0) (2021-07-07) + + +### chore + +* update to new multiformats ([#220](https://github.com/libp2p/js-libp2p-kad-dht/issues/220)) ([565eb00](https://github.com/libp2p/js-libp2p-kad-dht/commit/565eb003c0c5d165088d113f8caecc5f7a5a12ad)) + + +### BREAKING CHANGES + +* uses new multiformats CID class + + + +# [0.22.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.21.0...v0.22.0) (2021-04-28) + + + +# [0.21.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.6...v0.21.0) (2021-02-16) + + +### Features + +* add types and update all deps ([#214](https://github.com/libp2p/js-libp2p-kad-dht/issues/214)) ([7195282](https://github.com/libp2p/js-libp2p-kad-dht/commit/71952820ef3f737204b7a615db69ae680ef652a8)) + + + + +## [0.20.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.5...v0.20.6) (2021-01-26) + + + + +## [0.20.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.4...v0.20.5) (2021-01-21) + + +### Bug Fixes + +* do not throw on empty provider list ([#212](https://github.com/libp2p/js-libp2p-kad-dht/issues/212)) ([3c2096e](https://github.com/libp2p/js-libp2p-kad-dht/commit/3c2096e)) + + + + +## [0.20.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.3...v0.20.4) (2020-12-17) + + +### Features + +* adds removeLocal function ([#211](https://github.com/libp2p/js-libp2p-kad-dht/issues/211)) ([d0db16b](https://github.com/libp2p/js-libp2p-kad-dht/commit/d0db16b)) + + + + +## [0.20.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.2...v0.20.3) (2020-12-09) + + +### Features + +* adds custom multicodec protocol option ([#206](https://github.com/libp2p/js-libp2p-kad-dht/issues/206)) ([20d57b5](https://github.com/libp2p/js-libp2p-kad-dht/commit/20d57b5)) + + + + +## [0.20.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.1...v0.20.2) (2020-12-04) + + +### Features + +* onPut and onRemove events ([#205](https://github.com/libp2p/js-libp2p-kad-dht/issues/205)) ([b28afdd](https://github.com/libp2p/js-libp2p-kad-dht/commit/b28afdd)) + + + + +## [0.20.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.9...v0.20.1) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#202](https://github.com/libp2p/js-libp2p-kad-dht/issues/202)) ([989be87](https://github.com/libp2p/js-libp2p-kad-dht/commit/989be87)) + + +### BREAKING CHANGES + +* - Where node Buffers were returned, now Uint8Arrays are + +* chore: remove gh dep urls + + + + +# [0.20.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.9...v0.20.0) (2020-08-10) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#202](https://github.com/libp2p/js-libp2p-kad-dht/issues/202)) ([989be87](https://github.com/libp2p/js-libp2p-kad-dht/commit/989be87)) + + +### BREAKING CHANGES + +* - Where node Buffers were returned, now Uint8Arrays are + +* chore: remove gh dep urls + + + + +## [0.19.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.8...v0.19.9) (2020-07-10) + + +### Bug Fixes + +* actually send the add provider rpc with addresses ([#201](https://github.com/libp2p/js-libp2p-kad-dht/issues/201)) ([f3188be](https://github.com/libp2p/js-libp2p-kad-dht/commit/f3188be)) + + +### Features + +* add support for client mode ([#200](https://github.com/libp2p/js-libp2p-kad-dht/issues/200)) ([91f6e4f](https://github.com/libp2p/js-libp2p-kad-dht/commit/91f6e4f)) + + + + +## [0.19.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.7...v0.19.8) (2020-07-08) + + +### Bug Fixes + +* check for an existing connection before using the dialer ([#199](https://github.com/libp2p/js-libp2p-kad-dht/issues/199)) ([578c5d0](https://github.com/libp2p/js-libp2p-kad-dht/commit/578c5d0)) + + + + +## [0.19.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.6...v0.19.7) (2020-06-23) + + + + +## [0.19.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.5...v0.19.6) (2020-06-16) + + +### Bug Fixes + +* use utils.mapParallel for parallel processing of peers ([#166](https://github.com/libp2p/js-libp2p-kad-dht/issues/166)) ([534a2d9](https://github.com/libp2p/js-libp2p-kad-dht/commit/534a2d9)) + + + + +## [0.19.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.4...v0.19.5) (2020-06-05) + + +### Bug Fixes + +* providers leaking resources on dht construction ([#194](https://github.com/libp2p/js-libp2p-kad-dht/issues/194)) ([59f373a](https://github.com/libp2p/js-libp2p-kad-dht/commit/59f373a)) + + + + +## [0.19.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.3...v0.19.4) (2020-05-20) + + + + +## [0.19.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.2...v0.19.3) (2020-05-15) + + + + +## [0.19.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.1...v0.19.2) (2020-04-28) + + +### Bug Fixes + +* add buffer ([#185](https://github.com/libp2p/js-libp2p-kad-dht/issues/185)) ([a28d279](https://github.com/libp2p/js-libp2p-kad-dht/commit/a28d279)) + + + + +## [0.19.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.0...v0.19.1) (2020-04-27) + + + + +# [0.19.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.0-pre.0...v0.19.0) (2020-04-24) + + +### Chores + +* peer-discovery not using peer-info ([#180](https://github.com/libp2p/js-libp2p-kad-dht/issues/180)) ([f0fb212](https://github.com/libp2p/js-libp2p-kad-dht/commit/f0fb212)) + + +### BREAKING CHANGES + +* peer event emitted with id and multiaddrs properties instead of peer-info + + + + +# [0.19.0-pre.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.6...v0.19.0-pre.0) (2020-04-16) + + +### Chores + +* use new peer store api ([#179](https://github.com/libp2p/js-libp2p-kad-dht/issues/179)) ([194c701](https://github.com/libp2p/js-libp2p-kad-dht/commit/194c701)) + + +### BREAKING CHANGES + +* uses new peer-store api + + + + +## [0.18.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.5...v0.18.6) (2020-03-26) + + + + +## [0.18.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.4...v0.18.5) (2020-02-14) + + +### Bug Fixes + +* remove use of assert module ([#173](https://github.com/libp2p/js-libp2p-kad-dht/issues/173)) ([de85eb6](https://github.com/libp2p/js-libp2p-kad-dht/commit/de85eb6)) + + + + +## [0.18.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.3...v0.18.4) (2020-02-05) + + + + +## [0.18.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.2...v0.18.3) (2019-12-12) + + +### Bug Fixes + +* dont use peer ids in sets ([#165](https://github.com/libp2p/js-libp2p-kad-dht/issues/165)) ([e12e540](https://github.com/libp2p/js-libp2p-kad-dht/commit/e12e540)) + + + + +## [0.18.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.1...v0.18.2) (2019-12-06) + + +### Bug Fixes + +* get many should not fail if found locally ([#161](https://github.com/libp2p/js-libp2p-kad-dht/issues/161)) ([091db13](https://github.com/libp2p/js-libp2p-kad-dht/commit/091db13)) + + + + +## [0.18.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.0...v0.18.1) (2019-12-05) + + +### Bug Fixes + +* find providers should yield when found locally ([#160](https://github.com/libp2p/js-libp2p-kad-dht/issues/160)) ([e40834a](https://github.com/libp2p/js-libp2p-kad-dht/commit/e40834a)) + + + + +# [0.18.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.17.1...v0.18.0) (2019-11-30) + + +### Features + +* find providers and closest peers return async iterable ([#157](https://github.com/libp2p/js-libp2p-kad-dht/issues/157)) ([f0e6800](https://github.com/libp2p/js-libp2p-kad-dht/commit/f0e6800)) + + +### BREAKING CHANGES + +* API for find providers and closest peers return async iterable instead of an array of PeerInfo + + + + +## [0.17.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.17.0...v0.17.1) (2019-11-28) + + +### Bug Fixes + +* remove extraneous message size filter ([#156](https://github.com/libp2p/js-libp2p-kad-dht/issues/156)) ([58b6b36](https://github.com/libp2p/js-libp2p-kad-dht/commit/58b6b36)) + + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.16.1...v0.17.0) (2019-11-26) + + +### Bug Fixes + +* stop and start should not fail ([#152](https://github.com/libp2p/js-libp2p-kad-dht/issues/152)) ([eee2f61](https://github.com/libp2p/js-libp2p-kad-dht/commit/eee2f61)) + + +### Code Refactoring + +* async await ([#148](https://github.com/libp2p/js-libp2p-kad-dht/issues/148)) ([c49fa92](https://github.com/libp2p/js-libp2p-kad-dht/commit/c49fa92)) + + +### BREAKING CHANGES + +* Switch to using async/await and async iterators. + +Co-Authored-By: Jacob Heun + + + + +## [0.16.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.16.0...v0.16.1) (2019-10-21) + + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.3...v0.16.0) (2019-08-16) + + +### Code Refactoring + +* use async datastore ([#140](https://github.com/libp2p/js-libp2p-kad-dht/issues/140)) ([daf9b00](https://github.com/libp2p/js-libp2p-kad-dht/commit/daf9b00)) + + +### BREAKING CHANGES + +* The DHT now requires its datastore to have +a promise based api, instead of callbacks. Datastores that use +ipfs/interface-datastore@0.7 or later should be used. +https://github.com/ipfs/interface-datastore/releases/tag/v0.7.0 + + + + +## [0.15.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.2...v0.15.3) (2019-07-29) + + +### Bug Fixes + +* _findNProvidersAsync discarding search results ([#137](https://github.com/libp2p/js-libp2p-kad-dht/issues/137)) ([e656c6b](https://github.com/libp2p/js-libp2p-kad-dht/commit/e656c6b)) + + + + +## [0.15.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.1...v0.15.2) (2019-05-31) + + +### Bug Fixes + +* favour providers peerInfo over sender peerInfo in ADD_PROVIDER ([#129](https://github.com/libp2p/js-libp2p-kad-dht/issues/129)) ([6da26b0](https://github.com/libp2p/js-libp2p-kad-dht/commit/6da26b0)) + + + + +## [0.15.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.0...v0.15.1) (2019-05-30) + + +### Bug Fixes + +* in _findNProviders correctly calculate pathSize ([5841dfe](https://github.com/libp2p/js-libp2p-kad-dht/commit/5841dfe)) +* send correct payload in ADD_PROVIDER RPC ([#127](https://github.com/libp2p/js-libp2p-kad-dht/issues/127)) ([8d92d5a](https://github.com/libp2p/js-libp2p-kad-dht/commit/8d92d5a)) + + +### Features + +* use promisify-es6 instead of pify ([1d228e0](https://github.com/libp2p/js-libp2p-kad-dht/commit/1d228e0)) + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.15...v0.15.0) (2019-05-13) + + +### Chores + +* update cids dependency ([#117](https://github.com/libp2p/js-libp2p-kad-dht/issues/117)) ([04e213a](https://github.com/libp2p/js-libp2p-kad-dht/commit/04e213a)) + + +### BREAKING CHANGES + +* v1 CIDs are now encoded in base32 when stringified. + +https://github.com/ipfs/js-ipfs/issues/1995 + +License: MIT +Signed-off-by: Alan Shaw + + + + +## [0.14.15](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.14...v0.14.15) (2019-05-10) + + +### Bug Fixes + +* query stop with query not initialized ([b29dfde](https://github.com/libp2p/js-libp2p-kad-dht/commit/b29dfde)) + + + + +## [0.14.14](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.13...v0.14.14) (2019-05-08) + + +### Bug Fixes + +* performance improvements ([#107](https://github.com/libp2p/js-libp2p-kad-dht/issues/107)) ([ddf80fe](https://github.com/libp2p/js-libp2p-kad-dht/commit/ddf80fe)) + + + + +## [0.14.13](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.12...v0.14.13) (2019-04-22) + + +### Bug Fixes + +* random walk ([#104](https://github.com/libp2p/js-libp2p-kad-dht/issues/104)) ([9db17eb](https://github.com/libp2p/js-libp2p-kad-dht/commit/9db17eb)) + + +### Features + +* add delay support to random walk ([#101](https://github.com/libp2p/js-libp2p-kad-dht/issues/101)) ([7b70fa7](https://github.com/libp2p/js-libp2p-kad-dht/commit/7b70fa7)) +* limit scope of queries to k closest peers ([#97](https://github.com/libp2p/js-libp2p-kad-dht/issues/97)) ([f03619e](https://github.com/libp2p/js-libp2p-kad-dht/commit/f03619e)) + + + + +## [0.14.12](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.11...v0.14.12) (2019-04-04) + + +### Bug Fixes + +* stop running queries on shutdown ([#95](https://github.com/libp2p/js-libp2p-kad-dht/issues/95)) ([e137297](https://github.com/libp2p/js-libp2p-kad-dht/commit/e137297)) + + + + +## [0.14.11](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.10...v0.14.11) (2019-03-28) + + +### Bug Fixes + +* ensure queries stop after error or success ([#93](https://github.com/libp2p/js-libp2p-kad-dht/issues/93)) ([0e55b20](https://github.com/libp2p/js-libp2p-kad-dht/commit/0e55b20)) +* getMany with nvals=1 now goes out to network if no local val ([#91](https://github.com/libp2p/js-libp2p-kad-dht/issues/91)) ([478ee88](https://github.com/libp2p/js-libp2p-kad-dht/commit/478ee88)) + + + + +## [0.14.10](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.9...v0.14.10) (2019-03-27) + + +### Bug Fixes + +* false discovery ([#92](https://github.com/libp2p/js-libp2p-kad-dht/issues/92)) ([466c992](https://github.com/libp2p/js-libp2p-kad-dht/commit/466c992)) + + + + +## [0.14.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.8...v0.14.9) (2019-03-18) + + +### Bug Fixes + +* reduce bundle size ([#90](https://github.com/libp2p/js-libp2p-kad-dht/issues/90)) ([f79eeb2](https://github.com/libp2p/js-libp2p-kad-dht/commit/f79eeb2)) + + + + +## [0.14.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.7...v0.14.8) (2019-03-13) + + +### Bug Fixes + +* incoming message should not connect to peers ([#88](https://github.com/libp2p/js-libp2p-kad-dht/issues/88)) ([8c16b81](https://github.com/libp2p/js-libp2p-kad-dht/commit/8c16b81)) + + + + +## [0.14.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.6...v0.14.7) (2019-03-04) + + +### Bug Fixes + +* put query for closest peers ([#85](https://github.com/libp2p/js-libp2p-kad-dht/issues/85)) ([84a40cd](https://github.com/libp2p/js-libp2p-kad-dht/commit/84a40cd)) + + + + +## [0.14.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.5...v0.14.6) (2019-02-25) + + +### Bug Fixes + +* specify # of peers for successful put ([#72](https://github.com/libp2p/js-libp2p-kad-dht/issues/72)) ([97e8e60](https://github.com/libp2p/js-libp2p-kad-dht/commit/97e8e60)) + + +### Features + +* expose randomwalk parameters in config ([#77](https://github.com/libp2p/js-libp2p-kad-dht/issues/77)) ([dc5a67f](https://github.com/libp2p/js-libp2p-kad-dht/commit/dc5a67f)) + + + + +## [0.14.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.4...v0.14.5) (2019-02-05) + + +### Features + +* emit event on peer connected ([#66](https://github.com/libp2p/js-libp2p-kad-dht/issues/66)) ([ba0a537](https://github.com/libp2p/js-libp2p-kad-dht/commit/ba0a537)) + + + + +## [0.14.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.3...v0.14.4) (2019-01-14) + + + + +## [0.14.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.2...v0.14.3) (2019-01-04) + + + + +## [0.14.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.1...v0.14.2) (2019-01-04) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.0...v0.14.1) (2018-12-11) + + +### Bug Fixes + +* typo get many option ([#63](https://github.com/libp2p/js-libp2p-kad-dht/issues/63)) ([de5a9fb](https://github.com/libp2p/js-libp2p-kad-dht/commit/de5a9fb)) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.13.0...v0.14.0) (2018-12-11) + + +### Chores + +* update options timeout property ([#62](https://github.com/libp2p/js-libp2p-kad-dht/issues/62)) ([3046b54](https://github.com/libp2p/js-libp2p-kad-dht/commit/3046b54)) + + +### BREAKING CHANGES + +* get, getMany, findProviders and findPeer do not accept a timeout number anymore. It must be a property of an object options. + +Co-Authored-By: vasco-santos + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.12.1...v0.13.0) (2018-12-05) + + +### Bug Fixes + +* make 'find peer query' test reliable ([#58](https://github.com/libp2p/js-libp2p-kad-dht/issues/58)) ([54336dd](https://github.com/libp2p/js-libp2p-kad-dht/commit/54336dd)) + + +### Features + +* run queries on disjoint paths ([#37](https://github.com/libp2p/js-libp2p-kad-dht/issues/37)) ([#39](https://github.com/libp2p/js-libp2p-kad-dht/issues/39)) ([742b3fb](https://github.com/libp2p/js-libp2p-kad-dht/commit/742b3fb)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.12.0...v0.12.1) (2018-11-30) + + +### Features + +* allow configurable validators and selectors ([#57](https://github.com/libp2p/js-libp2p-kad-dht/issues/57)) ([b731a1d](https://github.com/libp2p/js-libp2p-kad-dht/commit/b731a1d)) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.11.1...v0.12.0) (2018-11-22) + + + + +## [0.11.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.11.0...v0.11.1) (2018-11-12) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.6...v0.11.0) (2018-11-09) + + +### Bug Fixes + +* record outdated local correction ([#49](https://github.com/libp2p/js-libp2p-kad-dht/issues/49)) ([d1869ed](https://github.com/libp2p/js-libp2p-kad-dht/commit/d1869ed)) + + +### Features + +* select first record when no selector function ([#51](https://github.com/libp2p/js-libp2p-kad-dht/issues/51)) ([683a903](https://github.com/libp2p/js-libp2p-kad-dht/commit/683a903)) + + + + +## [0.10.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.5...v0.10.6) (2018-10-25) + + + + +## [0.10.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.4...v0.10.5) (2018-10-01) + + +### Features + +* start random walk and allow configuration for disabling ([#42](https://github.com/libp2p/js-libp2p-kad-dht/issues/42)) ([abe9407](https://github.com/libp2p/js-libp2p-kad-dht/commit/abe9407)) + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.3...v0.10.4) (2018-09-27) + + +### Bug Fixes + +* find peer and providers options ([#45](https://github.com/libp2p/js-libp2p-kad-dht/issues/45)) ([bba7500](https://github.com/libp2p/js-libp2p-kad-dht/commit/bba7500)) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.2...v0.10.3) (2018-09-20) + + +### Bug Fixes + +* dht get options ([#40](https://github.com/libp2p/js-libp2p-kad-dht/issues/40)) ([0a2f9fe](https://github.com/libp2p/js-libp2p-kad-dht/commit/0a2f9fe)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.0...v0.10.2) (2018-08-29) + + +### Bug Fixes + +* dont read when just doing a write ([7a92139](https://github.com/libp2p/js-libp2p-kad-dht/commit/7a92139)) +* make findProviders treat timeout the same as findPeer ([#35](https://github.com/libp2p/js-libp2p-kad-dht/issues/35)) ([fcdb01d](https://github.com/libp2p/js-libp2p-kad-dht/commit/fcdb01d)) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.0...v0.10.1) (2018-07-13) + + +### Bug Fixes + +* dont read when just doing a write ([7a92139](https://github.com/libp2p/js-libp2p-kad-dht/commit/7a92139)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.9.0...v0.10.0) (2018-04-05) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.8.0...v0.9.0) (2018-03-15) + + +### Features + +* upgrade the discovery service to random-walk ([b8e0f72](https://github.com/libp2p/js-libp2p-kad-dht/commit/b8e0f72)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.7.0...v0.8.0) (2018-02-07) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.0...v0.7.0) (2018-02-07) + + +### Bug Fixes + +* release providers resources ([#23](https://github.com/libp2p/js-libp2p-kad-dht/issues/23)) ([ff87f4b](https://github.com/libp2p/js-libp2p-kad-dht/commit/ff87f4b)) + + +### Features + +* use libp2p-switch ([054e5e5](https://github.com/libp2p/js-libp2p-kad-dht/commit/054e5e5)) + + + + +## [0.6.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.2...v0.6.3) (2018-01-30) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.1...v0.6.2) (2018-01-30) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.0...v0.6.1) (2018-01-30) + + +### Bug Fixes + +* release providers resources ([#23](https://github.com/libp2p/js-libp2p-kad-dht/issues/23)) ([ff87f4b](https://github.com/libp2p/js-libp2p-kad-dht/commit/ff87f4b)) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.5.1...v0.6.0) (2017-11-09) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.5.0...v0.5.1) (2017-09-07) + + +### Features + +* replace protocol-buffers with protons ([#16](https://github.com/libp2p/js-libp2p-kad-dht/issues/16)) ([de259ff](https://github.com/libp2p/js-libp2p-kad-dht/commit/de259ff)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.4.1...v0.5.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#15](https://github.com/libp2p/js-libp2p-kad-dht/issues/15)) ([3870dd2](https://github.com/libp2p/js-libp2p-kad-dht/commit/3870dd2)) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.4.0...v0.4.1) (2017-07-22) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.3.0...v0.4.0) (2017-07-22) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.2.1...v0.3.0) (2017-07-17) + + +### Bug Fixes + +* no more circular dependency, become a good block of libp2p ([#13](https://github.com/libp2p/js-libp2p-kad-dht/issues/13)) ([810be4d](https://github.com/libp2p/js-libp2p-kad-dht/commit/810be4d)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.2.0...v0.2.1) (2017-07-13) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.1.0...v0.2.0) (2017-07-07) + + +### Features + +* using libp2p new state methods ([#12](https://github.com/libp2p/js-libp2p-kad-dht/issues/12)) ([982f789](https://github.com/libp2p/js-libp2p-kad-dht/commit/982f789)) + + + + +# [0.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/4bd1fbc...v0.1.0) (2017-04-07) + + +### Features + +* v0.1.0 ([4bd1fbc](https://github.com/libp2p/js-libp2p-kad-dht/commit/4bd1fbc)) diff --git a/packages/kad-dht/LICENSE b/packages/kad-dht/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/kad-dht/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/kad-dht/LICENSE-APACHE b/packages/kad-dht/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/kad-dht/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/kad-dht/LICENSE-MIT b/packages/kad-dht/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/kad-dht/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/kad-dht/README.md b/packages/kad-dht/README.md new file mode 100644 index 0000000000..d0cb29462c --- /dev/null +++ b/packages/kad-dht/README.md @@ -0,0 +1,103 @@ +# @libp2p/kad-dht + +[![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 the Kad-DHT for libp2p + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +```sh +> npm i @libp2p/kad-dht +``` + +### Use in Node.js + +```js +import { create } from '@libp2p/kad-dht' +``` + +## API + +See for the auto generated docs. + +The libp2p-kad-dht module offers 3 APIs: Peer Routing, Content Routing and Peer Discovery. + +### Custom secondary DHT in libp2p + +```js +import { createLibp2pNode } from 'libp2p' +import { kadDHT } from '@libp2p/kad-dht' + +const node = await createLibp2pNode({ + dht: kadDHT() + //... other config +}) +await node.start() + +for await (const event of node.dht.findPeer(node.peerId)) { + console.info(event) +} +``` + +Note that you may want to supply your own peer discovery function and datastore + +### Peer Routing + +[![](https://raw.githubusercontent.com/libp2p/interface-peer-routing/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/peer-routing) + +### Content Routing + +[![](https://raw.githubusercontent.com/libp2p/interface-content-routing/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/content-routing) + +### Peer Discovery + +[![](https://github.com/libp2p/interface-peer-discovery/blob/master/img/badge.png?raw=true)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/peer-discovery) + +## Spec + +js-libp2p-kad-dht follows the [libp2p/kad-dht spec](https://github.com/libp2p/specs/tree/master/kad-dht) and implements the algorithms described in the [IPFS DHT documentation](https://docs.ipfs.io/concepts/dht/). + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/kad-dht/package.json b/packages/kad-dht/package.json new file mode 100644 index 0000000000..d4feddee61 --- /dev/null +++ b/packages/kad-dht/package.json @@ -0,0 +1,132 @@ +{ + "name": "@libp2p/kad-dht", + "version": "9.3.6", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/message/dht.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "build": "aegir build", + "generate": "protons ./src/message/dht.proto", + "test": "aegir test", + "test:node": "aegir test -t node --cov", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "dep-check": "aegir dep-check -i protons -i events" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-address-manager": "^3.0.0", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-connection-manager": "^3.0.0", + "@libp2p/interface-content-routing": "^2.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/interface-peer-discovery": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.0", + "@libp2p/interface-peer-routing": "^1.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interface-registrar": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-collections": "^3.0.0", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/record": "^3.0.0", + "@libp2p/topology": "^4.0.0", + "@multiformats/multiaddr": "^12.1.3", + "@types/sinon": "^10.0.15", + "abortable-iterator": "^5.0.1", + "any-signal": "^4.1.1", + "datastore-core": "^9.0.1", + "events": "^3.3.0", + "hashlru": "^2.3.0", + "interface-datastore": "^8.2.0", + "it-all": "^3.0.2", + "it-drain": "^3.0.1", + "it-first": "^3.0.1", + "it-length": "^3.0.1", + "it-length-prefixed": "^9.0.1", + "it-map": "^3.0.2", + "it-merge": "^3.0.0", + "it-parallel": "^3.0.0", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "it-take": "^3.0.1", + "multiformats": "^11.0.2", + "p-defer": "^4.0.0", + "p-event": "^6.0.0", + "p-queue": "^7.3.4", + "private-ip": "^3.0.0", + "progress-events": "^1.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3", + "varint": "^6.0.0" + }, + "devDependencies": { + "@libp2p/interface-libp2p": "^3.0.0", + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@libp2p/peer-store": "^8.0.0", + "@types/lodash.random": "^3.2.6", + "@types/lodash.range": "^3.2.6", + "@types/varint": "^6.0.0", + "@types/which": "^3.0.0", + "aegir": "^39.0.10", + "datastore-level": "^10.0.0", + "delay": "^6.0.0", + "execa": "^7.0.0", + "it-filter": "^3.0.1", + "it-last": "^3.0.1", + "lodash.random": "^3.2.0", + "lodash.range": "^3.2.0", + "p-retry": "^5.0.0", + "p-wait-for": "^5.0.0", + "protons": "^7.0.2", + "sinon": "^15.1.0", + "ts-sinon": "^2.0.2", + "which": "^3.0.0" + }, + "browser": { + "./dist/src/routing-table/generated-prefix-list.js": "./dist/src/routing-table/generated-prefix-list-browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/kad-dht/src/constants.ts b/packages/kad-dht/src/constants.ts new file mode 100644 index 0000000000..12edd7f512 --- /dev/null +++ b/packages/kad-dht/src/constants.ts @@ -0,0 +1,57 @@ +// MaxRecordAge specifies the maximum time that any node will hold onto a record +// from the time its received. This does not apply to any other forms of validity that +// the record may contain. +// For example, a record may contain an ipns entry with an EOL saying its valid +// until the year 2020 (a great time in the future). For that record to stick around +// it must be rebroadcasted more frequently than once every 'MaxRecordAge' + +export const second = 1000 +export const minute = 60 * second +export const hour = 60 * minute + +export const MAX_RECORD_AGE = 36 * hour + +export const LAN_PREFIX = '/lan' + +export const PROTOCOL_PREFIX = '/ipfs' + +export const PROTOCOL_DHT = '/kad/1.0.0' + +export const RECORD_KEY_PREFIX = '/dht/record' + +export const PROVIDER_KEY_PREFIX = '/dht/provider' + +export const PROVIDERS_LRU_CACHE_SIZE = 256 + +export const PROVIDERS_VALIDITY = 24 * hour + +export const PROVIDERS_CLEANUP_INTERVAL = hour + +export const READ_MESSAGE_TIMEOUT = 10 * second + +// The number of records that will be retrieved on a call to getMany() +export const GET_MANY_RECORD_COUNT = 16 + +// K is the maximum number of requests to perform before returning failure +export const K = 20 + +// Alpha is the concurrency for asynchronous requests +export const ALPHA = 3 + +// How often we look for our closest DHT neighbours +export const QUERY_SELF_INTERVAL = Number(5 * minute) + +// How often we look for the first set of our closest DHT neighbours +export const QUERY_SELF_INITIAL_INTERVAL = Number(Number(second)) + +// How long to look for our closest DHT neighbours for +export const QUERY_SELF_TIMEOUT = Number(5 * second) + +// How often we try to find new peers +export const TABLE_REFRESH_INTERVAL = Number(5 * minute) + +// How how long to look for new peers for +export const TABLE_REFRESH_QUERY_TIMEOUT = Number(30 * second) + +// When a timeout is not specified, run a query for this long +export const DEFAULT_QUERY_TIMEOUT = Number(30 * second) diff --git a/packages/kad-dht/src/content-fetching/index.ts b/packages/kad-dht/src/content-fetching/index.ts new file mode 100644 index 0000000000..67c86dbde1 --- /dev/null +++ b/packages/kad-dht/src/content-fetching/index.ts @@ -0,0 +1,263 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { Libp2pRecord } from '@libp2p/record' +import { bestRecord } from '@libp2p/record/selectors' +import { verifyRecord } from '@libp2p/record/validators' +import map from 'it-map' +import parallel from 'it-parallel' +import { pipe } from 'it-pipe' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { + ALPHA +} from '../constants.js' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { + valueEvent, + queryErrorEvent +} from '../query/events.js' +import { createPutRecord, bufferToRecordKey } from '../utils.js' +import type { KadDHTComponents, Validators, Selectors, ValueEvent, QueryOptions, QueryEvent } from '../index.js' +import type { Network } from '../network.js' +import type { PeerRouting } from '../peer-routing/index.js' +import type { QueryManager } from '../query/manager.js' +import type { QueryFunc } from '../query/types.js' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Logger } from '@libp2p/logger' + +export interface ContentFetchingInit { + validators: Validators + selectors: Selectors + peerRouting: PeerRouting + queryManager: QueryManager + network: Network + lan: boolean +} + +export class ContentFetching { + private readonly log: Logger + private readonly components: KadDHTComponents + private readonly validators: Validators + private readonly selectors: Selectors + private readonly peerRouting: PeerRouting + private readonly queryManager: QueryManager + private readonly network: Network + + constructor (components: KadDHTComponents, init: ContentFetchingInit) { + const { validators, selectors, peerRouting, queryManager, network, lan } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:content-fetching`) + this.validators = validators + this.selectors = selectors + this.peerRouting = peerRouting + this.queryManager = queryManager + this.network = network + } + + async putLocal (key: Uint8Array, rec: Uint8Array): Promise { + const dsKey = bufferToRecordKey(key) + await this.components.datastore.put(dsKey, rec) + } + + /** + * Attempt to retrieve the value for the given key from + * the local datastore + */ + async getLocal (key: Uint8Array): Promise { + this.log('getLocal %b', key) + + const dsKey = bufferToRecordKey(key) + + this.log('fetching record for key %k', dsKey) + + const raw = await this.components.datastore.get(dsKey) + this.log('found %k in local datastore', dsKey) + + const rec = Libp2pRecord.deserialize(raw) + + await verifyRecord(this.validators, rec) + + return rec + } + + /** + * Send the best record found to any peers that have an out of date record + */ + async * sendCorrectionRecord (key: Uint8Array, vals: ValueEvent[], best: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + this.log('sendCorrection for %b', key) + const fixupRec = createPutRecord(key, best) + + for (const { value, from } of vals) { + // no need to do anything + if (uint8ArrayEquals(value, best)) { + this.log('record was ok') + continue + } + + // correct ourself + if (this.components.peerId.equals(from)) { + try { + const dsKey = bufferToRecordKey(key) + this.log(`Storing corrected record for key ${dsKey.toString()}`) + await this.components.datastore.put(dsKey, fixupRec.subarray()) + } catch (err: any) { + this.log.error('Failed error correcting self', err) + } + + continue + } + + // send correction + let sentCorrection = false + const request = new Message(MESSAGE_TYPE.PUT_VALUE, key, 0) + request.record = Libp2pRecord.deserialize(fixupRec) + + for await (const event of this.network.sendRequest(from, request, options)) { + if (event.name === 'PEER_RESPONSE' && (event.record != null) && uint8ArrayEquals(event.record.value, Libp2pRecord.deserialize(fixupRec).value)) { + sentCorrection = true + } + + yield event + } + + if (!sentCorrection) { + yield queryErrorEvent({ from, error: new CodeError('value not put correctly', 'ERR_PUT_VALUE_INVALID') }, options) + } + + this.log.error('Failed error correcting entry') + } + } + + /** + * Store the given key/value pair in the DHT + */ + async * put (key: Uint8Array, value: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + this.log('put key %b value %b', key, value) + + // create record in the dht format + const record = createPutRecord(key, value) + + // store the record locally + const dsKey = bufferToRecordKey(key) + this.log(`storing record for key ${dsKey.toString()}`) + await this.components.datastore.put(dsKey, record.subarray()) + + // put record to the closest peers + yield * pipe( + this.peerRouting.getClosestPeers(key, { signal: options.signal }), + (source) => map(source, (event) => { + return async () => { + if (event.name !== 'FINAL_PEER') { + return [event] + } + + const events = [] + + const msg = new Message(MESSAGE_TYPE.PUT_VALUE, key, 0) + msg.record = Libp2pRecord.deserialize(record) + + this.log('send put to %p', event.peer.id) + for await (const putEvent of this.network.sendRequest(event.peer.id, msg, options)) { + events.push(putEvent) + + if (putEvent.name !== 'PEER_RESPONSE') { + continue + } + + if (!(putEvent.record != null && uint8ArrayEquals(putEvent.record.value, Libp2pRecord.deserialize(record).value))) { + events.push(queryErrorEvent({ from: event.peer.id, error: new CodeError('value not put correctly', 'ERR_PUT_VALUE_INVALID') }, options)) + } + } + + return events + } + }), + (source) => parallel(source, { + ordered: false, + concurrency: ALPHA + }), + async function * (source) { + for await (const events of source) { + yield * events + } + } + ) + } + + /** + * Get the value to the given key + */ + async * get (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + this.log('get %b', key) + + const vals: ValueEvent[] = [] + + for await (const event of this.getMany(key, options)) { + if (event.name === 'VALUE') { + vals.push(event) + } + + yield event + } + + if (vals.length === 0) { + return + } + + const records = vals.map((v) => v.value) + let i = 0 + + try { + i = bestRecord(this.selectors, key, records) + } catch (err: any) { + // Assume the first record if no selector available + if (err.code !== 'ERR_NO_SELECTOR_FUNCTION_FOR_RECORD_KEY') { + throw err + } + } + + const best = records[i] + this.log('GetValue %b %b', key, best) + + if (best == null) { + throw new CodeError('best value was not found', 'ERR_NOT_FOUND') + } + + yield * this.sendCorrectionRecord(key, vals, best, options) + + yield vals[i] + } + + /** + * Get the `n` values to the given key without sorting + */ + async * getMany (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + this.log('getMany values for %b', key) + + try { + const localRec = await this.getLocal(key) + + yield valueEvent({ + value: localRec.value, + from: this.components.peerId + }, options) + } catch (err: any) { + this.log('error getting local value for %b', key, err) + } + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + const getValueQuery: QueryFunc = async function * ({ peer, signal }) { + for await (const event of self.peerRouting.getValueOrPeers(peer, key, { signal })) { + yield event + + if (event.name === 'PEER_RESPONSE' && (event.record != null)) { + yield valueEvent({ from: peer, value: event.record.value }, options) + } + } + } + + // we have peers, lets send the actual query to them + yield * this.queryManager.run(key, getValueQuery, options) + } +} diff --git a/packages/kad-dht/src/content-routing/index.ts b/packages/kad-dht/src/content-routing/index.ts new file mode 100644 index 0000000000..5e4a8e0d72 --- /dev/null +++ b/packages/kad-dht/src/content-routing/index.ts @@ -0,0 +1,205 @@ +import { logger } from '@libp2p/logger' +import map from 'it-map' +import parallel from 'it-parallel' +import { pipe } from 'it-pipe' +import { ALPHA } from '../constants.js' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { + queryErrorEvent, + peerResponseEvent, + providerEvent +} from '../query/events.js' +import type { KadDHTComponents, PeerResponseEvent, ProviderEvent, QueryEvent, QueryOptions } from '../index.js' +import type { Network } from '../network.js' +import type { PeerRouting } from '../peer-routing/index.js' +import type { Providers } from '../providers.js' +import type { QueryManager } from '../query/manager.js' +import type { QueryFunc } from '../query/types.js' +import type { RoutingTable } from '../routing-table/index.js' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Logger } from '@libp2p/logger' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { CID } from 'multiformats/cid' + +export interface ContentRoutingInit { + network: Network + peerRouting: PeerRouting + queryManager: QueryManager + routingTable: RoutingTable + providers: Providers + lan: boolean +} + +export class ContentRouting { + private readonly log: Logger + private readonly components: KadDHTComponents + private readonly network: Network + private readonly peerRouting: PeerRouting + private readonly queryManager: QueryManager + private readonly routingTable: RoutingTable + private readonly providers: Providers + + constructor (components: KadDHTComponents, init: ContentRoutingInit) { + const { network, peerRouting, queryManager, routingTable, providers, lan } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:content-routing`) + this.network = network + this.peerRouting = peerRouting + this.queryManager = queryManager + this.routingTable = routingTable + this.providers = providers + } + + /** + * Announce to the network that we can provide the value for a given key and + * are contactable on the given multiaddrs + */ + async * provide (key: CID, multiaddrs: Multiaddr[], options: QueryOptions = {}): AsyncGenerator { + this.log('provide %s', key) + + // Add peer as provider + await this.providers.addProvider(key, this.components.peerId) + + const msg = new Message(MESSAGE_TYPE.ADD_PROVIDER, key.multihash.bytes, 0) + msg.providerPeers = [{ + id: this.components.peerId, + multiaddrs, + protocols: [] + }] + + let sent = 0 + + const maybeNotifyPeer = (event: QueryEvent) => { + return async () => { + if (event.name !== 'FINAL_PEER') { + return [event] + } + + const events = [] + + this.log('putProvider %s to %p', key, event.peer.id) + + try { + this.log('sending provider record for %s to %p', key, event.peer.id) + + for await (const sendEvent of this.network.sendMessage(event.peer.id, msg, options)) { + if (sendEvent.name === 'PEER_RESPONSE') { + this.log('sent provider record for %s to %p', key, event.peer.id) + sent++ + } + + events.push(sendEvent) + } + } catch (err: any) { + this.log.error('error sending provide record to peer %p', event.peer.id, err) + events.push(queryErrorEvent({ from: event.peer.id, error: err }, options)) + } + + return events + } + } + + // Notify closest peers + yield * pipe( + this.peerRouting.getClosestPeers(key.multihash.bytes, options), + (source) => map(source, (event) => maybeNotifyPeer(event)), + (source) => parallel(source, { + ordered: false, + concurrency: ALPHA + }), + async function * (source) { + for await (const events of source) { + yield * events + } + } + ) + + this.log('sent provider records to %d peers', sent) + } + + /** + * Search the dht for up to `K` providers of the given CID. + */ + async * findProviders (key: CID, options: QueryOptions): AsyncGenerator { + const toFind = this.routingTable.kBucketSize + const target = key.multihash.bytes + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + this.log('findProviders %c', key) + + const provs = await this.providers.getProviders(key) + + // yield values if we have some, also slice because maybe we got lucky and already have too many? + if (provs.length > 0) { + const providers: PeerInfo[] = [] + + for (const peerId of provs.slice(0, toFind)) { + try { + const peer = await this.components.peerStore.get(peerId) + + providers.push({ + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + }) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + + this.log('no peer store entry for %p', peerId) + } + } + + yield peerResponseEvent({ from: this.components.peerId, messageType: MESSAGE_TYPE.GET_PROVIDERS, providers }, options) + yield providerEvent({ from: this.components.peerId, providers }, options) + } + + // All done + if (provs.length >= toFind) { + return + } + + /** + * The query function to use on this particular disjoint path + */ + const findProvidersQuery: QueryFunc = async function * ({ peer, signal }) { + const request = new Message(MESSAGE_TYPE.GET_PROVIDERS, target, 0) + + yield * self.network.sendRequest(peer, request, { + ...options, + signal + }) + } + + const providers = new Set(provs.map(p => p.toString())) + + for await (const event of this.queryManager.run(target, findProvidersQuery, options)) { + yield event + + if (event.name === 'PEER_RESPONSE') { + this.log('Found %d provider entries for %c and %d closer peers', event.providers.length, key, event.closer.length) + + const newProviders = [] + + for (const peer of event.providers) { + if (providers.has(peer.id.toString())) { + continue + } + + providers.add(peer.id.toString()) + newProviders.push(peer) + } + + if (newProviders.length > 0) { + yield providerEvent({ from: event.from, providers: newProviders }, options) + } + + if (providers.size === toFind) { + return + } + } + } + } +} diff --git a/packages/kad-dht/src/dual-kad-dht.ts b/packages/kad-dht/src/dual-kad-dht.ts new file mode 100644 index 0000000000..32a9f71d10 --- /dev/null +++ b/packages/kad-dht/src/dual-kad-dht.ts @@ -0,0 +1,400 @@ +import { type ContentRouting, contentRouting } from '@libp2p/interface-content-routing' +import { type PeerDiscovery, peerDiscovery, type PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import { type PeerRouting, peerRouting } from '@libp2p/interface-peer-routing' +import { CodeError } from '@libp2p/interfaces/errors' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import drain from 'it-drain' +import merge from 'it-merge' +import isPrivate from 'private-ip' +import { DefaultKadDHT } from './kad-dht.js' +import { queryErrorEvent } from './query/events.js' +import type { DualKadDHT, KadDHT, KadDHTComponents, KadDHTInit, QueryEvent, QueryOptions } from './index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { CID } from 'multiformats/cid' + +const log = logger('libp2p:kad-dht') + +/** + * Wrapper class to convert events into returned values + */ +class DHTContentRouting implements ContentRouting { + private readonly dht: KadDHT + + constructor (dht: KadDHT) { + this.dht = dht + } + + async provide (cid: CID, options: QueryOptions = {}): Promise { + await drain(this.dht.provide(cid, options)) + } + + async * findProviders (cid: CID, options: QueryOptions = {}): AsyncGenerator { + for await (const event of this.dht.findProviders(cid, options)) { + if (event.name === 'PROVIDER') { + yield * event.providers + } + } + } + + async put (key: Uint8Array, value: Uint8Array, options?: QueryOptions): Promise { + await drain(this.dht.put(key, value, options)) + } + + async get (key: Uint8Array, options?: QueryOptions): Promise { + for await (const event of this.dht.get(key, options)) { + if (event.name === 'VALUE') { + return event.value + } + } + + throw new CodeError('Not found', 'ERR_NOT_FOUND') + } +} + +/** + * Wrapper class to convert events into returned values + */ +class DHTPeerRouting implements PeerRouting { + private readonly dht: KadDHT + + constructor (dht: KadDHT) { + this.dht = dht + } + + async findPeer (peerId: PeerId, options: QueryOptions = {}): Promise { + for await (const event of this.dht.findPeer(peerId, options)) { + if (event.name === 'FINAL_PEER') { + return event.peer + } + } + + throw new CodeError('Not found', 'ERR_NOT_FOUND') + } + + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncIterable { + for await (const event of this.dht.getClosestPeers(key, options)) { + if (event.name === 'FINAL_PEER') { + yield event.peer + } + } + } +} + +// see https://github.com/multiformats/multiaddr/blob/master/protocols.csv +const P2P_CIRCUIT_CODE = 290 +const DNS4_CODE = 54 +const DNS6_CODE = 55 +const DNSADDR_CODE = 56 +const IP4_CODE = 4 +const IP6_CODE = 41 + +function multiaddrIsPublic (multiaddr: Multiaddr): boolean { + const tuples = multiaddr.stringTuples() + + // p2p-circuit should not enable server mode + for (const tuple of tuples) { + if (tuple[0] === P2P_CIRCUIT_CODE) { + return false + } + } + + // dns4 or dns6 or dnsaddr + if (tuples[0][0] === DNS4_CODE || tuples[0][0] === DNS6_CODE || tuples[0][0] === DNSADDR_CODE) { + log('%m is public %s', multiaddr, true) + + return true + } + + // ip4 or ip6 + if (tuples[0][0] === IP4_CODE || tuples[0][0] === IP6_CODE) { + const result = isPrivate(`${tuples[0][1]}`) + const isPublic = result == null || !result + + log('%m is public %s', multiaddr, isPublic) + + return isPublic + } + + return false +} + +/** + * A DHT implementation modelled after Kademlia with S/Kademlia modifications. + * Original implementation in go: https://github.com/libp2p/go-libp2p-kad-dht. + */ +export class DefaultDualKadDHT extends EventEmitter implements DualKadDHT, PeerDiscovery { + public readonly wan: DefaultKadDHT + public readonly lan: DefaultKadDHT + public readonly components: KadDHTComponents + private readonly contentRouting: ContentRouting + private readonly peerRouting: PeerRouting + + constructor (components: KadDHTComponents, init: KadDHTInit = {}) { + super() + + this.components = components + + this.wan = new DefaultKadDHT(components, { + protocolPrefix: '/ipfs', + ...init, + lan: false + }) + this.lan = new DefaultKadDHT(components, { + protocolPrefix: '/ipfs', + ...init, + clientMode: false, + lan: true + }) + + this.contentRouting = new DHTContentRouting(this) + this.peerRouting = new DHTPeerRouting(this) + + // handle peers being discovered during processing of DHT messages + this.wan.addEventListener('peer', (evt) => { + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + }) + this.lan.addEventListener('peer', (evt) => { + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + }) + + // if client mode has not been explicitly specified, auto-switch to server + // mode when the node's peer data is updated with publicly dialable addresses + if (init.clientMode == null) { + components.events.addEventListener('self:peer:update', (evt) => { + log('received update of self-peer info') + const hasPublicAddress = evt.detail.peer.addresses + .some(({ multiaddr }) => { + const isPublic = multiaddrIsPublic(multiaddr) + + log('%m is public %s', multiaddr, isPublic) + + return isPublic + }) + + this.getMode() + .then(async mode => { + if (hasPublicAddress && mode === 'client') { + await this.setMode('server') + } else if (mode === 'server' && !hasPublicAddress) { + await this.setMode('client') + } + }) + .catch(err => { + log.error('error setting dht server mode', err) + }) + }) + } + } + + readonly [Symbol.toStringTag] = '@libp2p/dual-kad-dht' + + get [contentRouting] (): ContentRouting { + return this.contentRouting + } + + get [peerRouting] (): PeerRouting { + return this.peerRouting + } + + get [peerDiscovery] (): PeerDiscovery { + return this + } + + /** + * Is this DHT running. + */ + isStarted (): boolean { + return this.wan.isStarted() && this.lan.isStarted() + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async getMode (): Promise<'client' | 'server'> { + return this.wan.getMode() + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async setMode (mode: 'client' | 'server'): Promise { + await this.wan.setMode(mode) + } + + /** + * Start listening to incoming connections. + */ + async start (): Promise { + await Promise.all([ + this.lan.start(), + this.wan.start() + ]) + } + + /** + * Stop accepting incoming connections and sending outgoing + * messages. + */ + async stop (): Promise { + await Promise.all([ + this.lan.stop(), + this.wan.stop() + ]) + } + + /** + * Store the given key/value pair in the DHT + */ + async * put (key: Uint8Array, value: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + for await (const event of merge( + this.lan.put(key, value, options), + this.wan.put(key, value, options) + )) { + yield event + } + } + + /** + * Get the value that corresponds to the passed key + */ + async * get (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + let queriedPeers = false + let foundValue = false + + for await (const event of merge( + this.lan.get(key, options), + this.wan.get(key, options) + )) { + yield event + + if (event.name === 'DIAL_PEER') { + queriedPeers = true + } + + if (event.name === 'VALUE') { + queriedPeers = true + + if (event.value != null) { + foundValue = true + } + } + + if (event.name === 'SEND_QUERY') { + queriedPeers = true + } + } + + if (!queriedPeers) { + throw new CodeError('No peers found in routing table!', 'ERR_NO_PEERS_IN_ROUTING_TABLE') + } + + if (!foundValue) { + yield queryErrorEvent({ + from: this.components.peerId, + error: new CodeError('Not found', 'ERR_NOT_FOUND') + }, options) + } + } + + // ----------- Content Routing + + /** + * Announce to the network that we can provide given key's value + */ + async * provide (key: CID, options: QueryOptions = {}): AsyncGenerator { + let sent = 0 + let success = 0 + const errors = [] + + const dhts = [this.lan] + + // only run provide on the wan if we are in server mode + if ((await this.wan.getMode()) === 'server') { + dhts.push(this.wan) + } + + for await (const event of merge(...dhts.map(dht => dht.provide(key, options)))) { + yield event + + if (event.name === 'SEND_QUERY') { + sent++ + } + + if (event.name === 'QUERY_ERROR') { + errors.push(event.error) + } + + if (event.name === 'PEER_RESPONSE' && event.messageName === 'ADD_PROVIDER') { + log('sent provider record for %s to %p', key, event.from) + success++ + } + } + + if (success === 0) { + if (errors.length > 0) { + // if all sends failed, throw an error to inform the caller + throw new CodeError(`Failed to provide to ${errors.length} of ${sent} peers`, 'ERR_PROVIDES_FAILED', { errors }) + } + + throw new CodeError('Failed to provide - no peers found', 'ERR_PROVIDES_FAILED') + } + } + + /** + * Search the dht for up to `K` providers of the given CID + */ + async * findProviders (key: CID, options: QueryOptions = {}): AsyncGenerator { + yield * merge( + this.lan.findProviders(key, options), + this.wan.findProviders(key, options) + ) + } + + // ----------- Peer Routing ----------- + + /** + * Search for a peer with the given ID + */ + async * findPeer (id: PeerId, options: QueryOptions = {}): AsyncGenerator { + let queriedPeers = false + + for await (const event of merge( + this.lan.findPeer(id, options), + this.wan.findPeer(id, options) + )) { + yield event + + if (event.name === 'SEND_QUERY' || event.name === 'FINAL_PEER') { + queriedPeers = true + } + } + + if (!queriedPeers) { + throw new CodeError('Peer lookup failed', 'ERR_LOOKUP_FAILED') + } + } + + /** + * Kademlia 'node lookup' operation + */ + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * merge( + this.lan.getClosestPeers(key, options), + this.wan.getClosestPeers(key, options) + ) + } + + async refreshRoutingTable (): Promise { + await Promise.all([ + this.lan.refreshRoutingTable(), + this.wan.refreshRoutingTable() + ]) + } +} diff --git a/packages/kad-dht/src/index.ts b/packages/kad-dht/src/index.ts new file mode 100644 index 0000000000..81ea30d402 --- /dev/null +++ b/packages/kad-dht/src/index.ts @@ -0,0 +1,321 @@ +import { DefaultDualKadDHT } from './dual-kad-dht.js' +import type { ProvidersInit } from './providers.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Registrar } from '@libp2p/interface-registrar' +import type { AbortOptions } from '@libp2p/interfaces' +import type { EventEmitter } from '@libp2p/interfaces/events' +import type { Datastore } from 'interface-datastore' +import type { CID } from 'multiformats/cid' +import type { ProgressOptions, ProgressEvent } from 'progress-events' + +/** + * The types of events emitted during DHT queries + */ +export enum EventTypes { + SEND_QUERY = 0, + PEER_RESPONSE, + FINAL_PEER, + QUERY_ERROR, + PROVIDER, + VALUE, + ADD_PEER, + DIAL_PEER +} + +/** + * The types of messages sent to peers during DHT queries + */ +export enum MessageType { + PUT_VALUE = 0, + GET_VALUE, + ADD_PROVIDER, + GET_PROVIDERS, + FIND_NODE, + PING +} + +export type MessageName = keyof typeof MessageType + +export interface DHTRecord { + key: Uint8Array + value: Uint8Array + timeReceived?: Date +} + +export type DHTProgressEvents = + ProgressEvent<'kad-dht:query:send-query', SendQueryEvent> | + ProgressEvent<'kad-dht:query:peer-response', PeerResponseEvent> | + ProgressEvent<'kad-dht:query:final-peer', FinalPeerEvent> | + ProgressEvent<'kad-dht:query:query-error', QueryErrorEvent> | + ProgressEvent<'kad-dht:query:provider', ProviderEvent> | + ProgressEvent<'kad-dht:query:value', ValueEvent> | + ProgressEvent<'kad-dht:query:add-peer', AddPeerEvent> | + ProgressEvent<'kad-dht:query:dial-peer', DialPeerEvent> + +export interface QueryOptions extends AbortOptions, ProgressOptions { + queryFuncTimeout?: number +} + +/** + * Emitted when sending queries to remote peers + */ +export interface SendQueryEvent { + to: PeerId + type: EventTypes.SEND_QUERY + name: 'SEND_QUERY' + messageName: keyof typeof MessageType + messageType: MessageType +} + +/** + * Emitted when query responses are received form remote peers. Depending on the query + * these events may be followed by a `FinalPeerEvent`, a `ValueEvent` or a `ProviderEvent`. + */ +export interface PeerResponseEvent { + from: PeerId + type: EventTypes.PEER_RESPONSE + name: 'PEER_RESPONSE' + messageName: keyof typeof MessageType + messageType: MessageType + closer: PeerInfo[] + providers: PeerInfo[] + record?: DHTRecord +} + +/** + * Emitted at the end of a `findPeer` query + */ +export interface FinalPeerEvent { + from: PeerId + peer: PeerInfo + type: EventTypes.FINAL_PEER + name: 'FINAL_PEER' +} + +/** + * Something went wrong with the query + */ +export interface QueryErrorEvent { + from: PeerId + type: EventTypes.QUERY_ERROR + name: 'QUERY_ERROR' + error: Error +} + +/** + * Emitted when providers are found + */ +export interface ProviderEvent { + from: PeerId + type: EventTypes.PROVIDER + name: 'PROVIDER' + providers: PeerInfo[] +} + +/** + * Emitted when values are found + */ +export interface ValueEvent { + from: PeerId + type: EventTypes.VALUE + name: 'VALUE' + value: Uint8Array +} + +/** + * Emitted when peers are added to a query + */ +export interface AddPeerEvent { + type: EventTypes.ADD_PEER + name: 'ADD_PEER' + peer: PeerId +} + +/** + * Emitted when peers are dialled as part of a query + */ +export interface DialPeerEvent { + peer: PeerId + type: EventTypes.DIAL_PEER + name: 'DIAL_PEER' +} + +export type QueryEvent = SendQueryEvent | PeerResponseEvent | FinalPeerEvent | QueryErrorEvent | ProviderEvent | ValueEvent | AddPeerEvent | DialPeerEvent + +export interface RoutingTable { + size: number +} + +export interface KadDHT { + /** + * Get a value from the DHT, the final ValueEvent will be the best value + */ + get: (key: Uint8Array, options?: QueryOptions) => AsyncIterable + + /** + * Find providers of a given CID + */ + findProviders: (key: CID, options?: QueryOptions) => AsyncIterable + + /** + * Find a peer on the DHT + */ + findPeer: (id: PeerId, options?: QueryOptions) => AsyncIterable + + /** + * Find the closest peers to the passed key + */ + 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 + + /** + * Store the passed value under the passed key on the DHT + */ + put: (key: Uint8Array, value: Uint8Array, options?: QueryOptions) => AsyncIterable + + /** + * Returns the mode this node is in + */ + getMode: () => Promise<'client' | 'server'> + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + setMode: (mode: 'client' | 'server') => Promise + + /** + * Force a routing table refresh + */ + refreshRoutingTable: () => Promise +} + +export interface SingleKadDHT extends KadDHT { + routingTable: RoutingTable +} + +export interface DualKadDHT extends KadDHT { + wan: SingleKadDHT + lan: SingleKadDHT +} + +/** + * A selector function takes a DHT key and a list of records and returns the + * index of the best record in the list + */ +export interface SelectFn { (key: Uint8Array, records: Uint8Array[]): number } + +/** + * A validator function takes a DHT key and the value of the record for that key + * and throws if the record is invalid + */ +export interface ValidateFn { (key: Uint8Array, value: Uint8Array): Promise } + +/** + * Selectors are a map of key prefixes to selector functions + */ +export type Selectors = Record + +/** + * Validators are a map of key prefixes to validator functions + */ +export type Validators = Record + +export interface KadDHTInit { + /** + * How many peers to store in each kBucket (default 20) + */ + kBucketSize?: number + + /** + * Whether to start up as a DHT client or server + */ + clientMode?: boolean + + /** + * Record selectors + */ + selectors?: Selectors + + /** + * Record validators + */ + validators?: Validators + + /** + * How often to query our own PeerId in order to ensure we have a + * good view on the KAD address space local to our PeerId + */ + querySelfInterval?: number + + /** + * During startup we run the self-query at a shorter interval to ensure + * the containing node can respond to queries quickly. Set this interval + * here in ms (default: 1000) + */ + initialQuerySelfInterval?: number + + /** + * After startup by default all queries will be paused until the initial + * self-query has run and there are some peers in the routing table. + * + * Pass true here to disable this behaviour. (default: false) + */ + allowQueryWithZeroPeers?: boolean + + /** + * A custom protocol prefix to use (default: '/ipfs') + */ + protocolPrefix?: string + + /** + * How long to wait in ms when pinging DHT peers to decide if they + * should be evicted from the routing table or not (default 10000) + */ + pingTimeout?: number + + /** + * How many peers to ping in parallel when deciding if they should + * be evicted from the routing table or not (default 10) + */ + pingConcurrency?: number + + /** + * How many parallel incoming streams to allow on the DHT protocol per-connection + */ + maxInboundStreams?: number + + /** + * How many parallel outgoing streams to allow on the DHT protocol per-connection + */ + maxOutboundStreams?: number + + /** + * Initialization options for the Providers component + */ + providers?: ProvidersInit +} + +export interface KadDHTComponents { + peerId: PeerId + registrar: Registrar + addressManager: AddressManager + peerStore: PeerStore + metrics?: Metrics + connectionManager: ConnectionManager + datastore: Datastore + events: EventEmitter +} + +export function kadDHT (init?: KadDHTInit): (components: KadDHTComponents) => DualKadDHT { + return (components: KadDHTComponents) => new DefaultDualKadDHT(components, init) +} diff --git a/packages/kad-dht/src/kad-dht.ts b/packages/kad-dht/src/kad-dht.ts new file mode 100644 index 0000000000..b0e40154f0 --- /dev/null +++ b/packages/kad-dht/src/kad-dht.ts @@ -0,0 +1,364 @@ +import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' +import { type Logger, logger } from '@libp2p/logger' +import { selectors as recordSelectors } from '@libp2p/record/selectors' +import { validators as recordValidators } from '@libp2p/record/validators' +import pDefer from 'p-defer' +import { PROTOCOL_DHT, PROTOCOL_PREFIX, LAN_PREFIX } from './constants.js' +import { ContentFetching } from './content-fetching/index.js' +import { ContentRouting } from './content-routing/index.js' +import { Network } from './network.js' +import { PeerRouting } from './peer-routing/index.js' +import { Providers } from './providers.js' +import { QueryManager } from './query/manager.js' +import { QuerySelf } from './query-self.js' +import { RoutingTable } from './routing-table/index.js' +import { RoutingTableRefresh } from './routing-table/refresh.js' +import { RPC } from './rpc/index.js' +import { TopologyListener } from './topology-listener.js' +import { + removePrivateAddresses, + removePublicAddresses +} from './utils.js' +import type { KadDHTComponents, KadDHTInit, QueryOptions, Validators, Selectors, KadDHT, QueryEvent } from './index.js' +import type { PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { CID } from 'multiformats/cid' + +export const DEFAULT_MAX_INBOUND_STREAMS = 32 +export const DEFAULT_MAX_OUTBOUND_STREAMS = 64 + +export interface SingleKadDHTInit extends KadDHTInit { + /** + * Whether to start up in lan or wan mode + */ + lan?: boolean +} + +/** + * A DHT implementation modelled after Kademlia with S/Kademlia modifications. + * Original implementation in go: https://github.com/libp2p/go-libp2p-kad-dht. + */ +export class DefaultKadDHT extends EventEmitter implements KadDHT { + public protocol: string + public routingTable: RoutingTable + public providers: Providers + public network: Network + public peerRouting: PeerRouting + + public readonly components: KadDHTComponents + private readonly log: Logger + private running: boolean + private readonly kBucketSize: number + private clientMode: boolean + private readonly lan: boolean + private readonly validators: Validators + private readonly selectors: Selectors + private readonly queryManager: QueryManager + private readonly contentFetching: ContentFetching + private readonly contentRouting: ContentRouting + private readonly routingTableRefresh: RoutingTableRefresh + private readonly rpc: RPC + private readonly topologyListener: TopologyListener + private readonly querySelf: QuerySelf + private readonly maxInboundStreams: number + private readonly maxOutboundStreams: number + + /** + * Create a new KadDHT + */ + constructor (components: KadDHTComponents, init: SingleKadDHTInit) { + super() + + const { + kBucketSize, + clientMode, + validators, + selectors, + querySelfInterval, + lan, + protocolPrefix, + pingTimeout, + pingConcurrency, + maxInboundStreams, + maxOutboundStreams, + providers: providersInit + } = init + + this.running = false + this.components = components + this.lan = Boolean(lan) + this.log = logger(`libp2p:kad-dht:${lan === true ? 'lan' : 'wan'}`) + this.protocol = `${protocolPrefix ?? PROTOCOL_PREFIX}${lan === true ? LAN_PREFIX : ''}${PROTOCOL_DHT}` + this.kBucketSize = kBucketSize ?? 20 + this.clientMode = clientMode ?? true + this.maxInboundStreams = maxInboundStreams ?? DEFAULT_MAX_INBOUND_STREAMS + this.maxOutboundStreams = maxOutboundStreams ?? DEFAULT_MAX_OUTBOUND_STREAMS + this.routingTable = new RoutingTable(components, { + kBucketSize, + lan: this.lan, + pingTimeout, + pingConcurrency, + protocol: this.protocol + }) + + this.providers = new Providers(components, providersInit ?? {}) + + this.validators = { + ...recordValidators, + ...validators + } + this.selectors = { + ...recordSelectors, + ...selectors + } + this.network = new Network(components, { + protocol: this.protocol, + lan: this.lan + }) + + // all queries should wait for the initial query-self query to run so we have + // some peers and don't force consumers to use arbitrary timeouts + const initialQuerySelfHasRun = pDefer() + + // if the user doesn't want to wait for query peers, resolve the initial + // self-query promise immediately + if (init.allowQueryWithZeroPeers === true) { + initialQuerySelfHasRun.resolve() + } + + this.queryManager = new QueryManager(components, { + // Number of disjoint query paths to use - This is set to `kBucketSize/2` per the S/Kademlia paper + disjointPaths: Math.ceil(this.kBucketSize / 2), + lan, + initialQuerySelfHasRun, + routingTable: this.routingTable + }) + + // DHT components + this.peerRouting = new PeerRouting(components, { + routingTable: this.routingTable, + network: this.network, + validators: this.validators, + queryManager: this.queryManager, + lan: this.lan + }) + this.contentFetching = new ContentFetching(components, { + validators: this.validators, + selectors: this.selectors, + peerRouting: this.peerRouting, + queryManager: this.queryManager, + network: this.network, + lan: this.lan + }) + this.contentRouting = new ContentRouting(components, { + network: this.network, + peerRouting: this.peerRouting, + queryManager: this.queryManager, + routingTable: this.routingTable, + providers: this.providers, + lan: this.lan + }) + this.routingTableRefresh = new RoutingTableRefresh({ + peerRouting: this.peerRouting, + routingTable: this.routingTable, + lan: this.lan + }) + this.rpc = new RPC(components, { + routingTable: this.routingTable, + providers: this.providers, + peerRouting: this.peerRouting, + validators: this.validators, + lan: this.lan + }) + this.topologyListener = new TopologyListener(components, { + protocol: this.protocol, + lan: this.lan + }) + this.querySelf = new QuerySelf(components, { + peerRouting: this.peerRouting, + interval: querySelfInterval, + initialInterval: init.initialQuerySelfInterval, + lan: this.lan, + initialQuerySelfHasRun, + routingTable: this.routingTable + }) + + // handle peers being discovered during processing of DHT messages + this.network.addEventListener('peer', (evt) => { + const peerData = evt.detail + + this.onPeerConnect(peerData).catch(err => { + this.log.error('could not add %p to routing table', peerData.id, err) + }) + + this.dispatchEvent(new CustomEvent('peer', { + detail: peerData + })) + }) + + // handle peers being discovered via other peer discovery mechanisms + this.topologyListener.addEventListener('peer', (evt) => { + const peerId = evt.detail + + Promise.resolve().then(async () => { + const peer = await this.components.peerStore.get(peerId) + + const peerData = { + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + } + + await this.onPeerConnect(peerData) + }).catch(err => { + this.log.error('could not add %p to routing table', peerId, err) + }) + }) + } + + async onPeerConnect (peerData: PeerInfo): Promise { + this.log('peer %p connected with protocols', peerData.id, peerData.protocols) + + if (this.lan) { + peerData = removePublicAddresses(peerData) + } else { + peerData = removePrivateAddresses(peerData) + } + + if (peerData.multiaddrs.length === 0) { + this.log('ignoring %p as they do not have any %s addresses in %s', peerData.id, this.lan ? 'private' : 'public', peerData.multiaddrs.map(addr => addr.toString())) + return + } + + try { + await this.routingTable.add(peerData.id) + } catch (err: any) { + this.log.error('could not add %p to routing table', peerData.id, err) + } + } + + /** + * Is this DHT running. + */ + isStarted (): boolean { + return this.running + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async getMode (): Promise<'client' | 'server'> { + return this.clientMode ? 'client' : 'server' + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async setMode (mode: 'client' | 'server'): Promise { + await this.components.registrar.unhandle(this.protocol) + + if (mode === 'client') { + this.log('enabling client mode') + this.clientMode = true + } else { + this.log('enabling server mode') + this.clientMode = false + await this.components.registrar.handle(this.protocol, this.rpc.onIncomingStream.bind(this.rpc), { + maxInboundStreams: this.maxInboundStreams, + maxOutboundStreams: this.maxOutboundStreams + }) + } + } + + /** + * Start listening to incoming connections. + */ + async start (): Promise { + this.running = true + + // Only respond to queries when not in client mode + await this.setMode(this.clientMode ? 'client' : 'server') + + await Promise.all([ + this.providers.start(), + this.queryManager.start(), + this.network.start(), + this.routingTable.start(), + this.topologyListener.start() + ]) + + this.querySelf.start() + + await this.routingTableRefresh.start() + } + + /** + * Stop accepting incoming connections and sending outgoing + * messages. + */ + async stop (): Promise { + this.running = false + + this.querySelf.stop() + + await Promise.all([ + this.providers.stop(), + this.queryManager.stop(), + this.network.stop(), + this.routingTable.stop(), + this.routingTableRefresh.stop(), + this.topologyListener.stop() + ]) + } + + /** + * Store the given key/value pair in the DHT + */ + async * put (key: Uint8Array, value: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentFetching.put(key, value, options) + } + + /** + * Get the value that corresponds to the passed key + */ + async * get (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentFetching.get(key, options) + } + + // ----------- Content Routing + + /** + * Announce to the network that we can provide given key's value + */ + async * provide (key: CID, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentRouting.provide(key, this.components.addressManager.getAddresses(), options) + } + + /** + * Search the dht for providers of the given CID + */ + async * findProviders (key: CID, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentRouting.findProviders(key, options) + } + + // ----------- Peer Routing ----------- + + /** + * Search for a peer with the given ID + */ + async * findPeer (id: PeerId, options: QueryOptions = {}): AsyncGenerator { + yield * this.peerRouting.findPeer(id, options) + } + + /** + * Kademlia 'node lookup' operation + */ + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * this.peerRouting.getClosestPeers(key, options) + } + + async refreshRoutingTable (): Promise { + this.routingTableRefresh.refreshTable(true) + } +} diff --git a/packages/kad-dht/src/message/dht.proto b/packages/kad-dht/src/message/dht.proto new file mode 100644 index 0000000000..160735b952 --- /dev/null +++ b/packages/kad-dht/src/message/dht.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; +// can't use, because protocol-buffers doesn't support imports +// so we have to duplicate for now :( +// import "record.proto"; + +message Record { + // adjusted for javascript + optional bytes key = 1; + optional bytes value = 2; + optional bytes author = 3; + optional bytes signature = 4; + optional string timeReceived = 5; +} + +message Message { + enum MessageType { + PUT_VALUE = 0; + GET_VALUE = 1; + ADD_PROVIDER = 2; + GET_PROVIDERS = 3; + FIND_NODE = 4; + PING = 5; + } + + enum ConnectionType { + // sender does not have a connection to peer, and no extra information (default) + NOT_CONNECTED = 0; + + // sender has a live connection to peer + CONNECTED = 1; + + // sender recently connected to peer + CAN_CONNECT = 2; + + // sender recently tried to connect to peer repeatedly but failed to connect + // ("try" here is loose, but this should signal "made strong effort, failed") + CANNOT_CONNECT = 3; + } + + message Peer { + // ID of a given peer. + optional bytes id = 1; + + // multiaddrs for a given peer + repeated bytes addrs = 2; + + // used to signal the sender's connection capabilities to the peer + optional ConnectionType connection = 3; + } + + // defines what type of message it is. + optional MessageType type = 1; + + // defines what coral cluster level this query/response belongs to. + // in case we want to implement coral's cluster rings in the future. + optional int32 clusterLevelRaw = 10; + + // Used to specify the key associated with this message. + // PUT_VALUE, GET_VALUE, ADD_PROVIDER, GET_PROVIDERS + // adjusted for javascript + optional bytes key = 2; + + // Used to return a value + // PUT_VALUE, GET_VALUE + // adjusted Record to bytes for js + optional bytes record = 3; + + // Used to return peers closer to a key in a query + // GET_VALUE, GET_PROVIDERS, FIND_NODE + repeated Peer closerPeers = 8; + + // Used to return Providers + // GET_VALUE, ADD_PROVIDER, GET_PROVIDERS + repeated Peer providerPeers = 9; +} diff --git a/packages/kad-dht/src/message/dht.ts b/packages/kad-dht/src/message/dht.ts new file mode 100644 index 0000000000..af919307e3 --- /dev/null +++ b/packages/kad-dht/src/message/dht.ts @@ -0,0 +1,331 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Record { + key?: Uint8Array + value?: Uint8Array + author?: Uint8Array + signature?: Uint8Array + timeReceived?: string +} + +export namespace Record { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.key != null) { + w.uint32(10) + w.bytes(obj.key) + } + + if (obj.value != null) { + w.uint32(18) + w.bytes(obj.value) + } + + if (obj.author != null) { + w.uint32(26) + w.bytes(obj.author) + } + + if (obj.signature != null) { + w.uint32(34) + w.bytes(obj.signature) + } + + if (obj.timeReceived != null) { + w.uint32(42) + w.string(obj.timeReceived) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.bytes() + break + case 2: + obj.value = reader.bytes() + break + case 3: + obj.author = reader.bytes() + break + case 4: + obj.signature = reader.bytes() + break + case 5: + obj.timeReceived = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Record.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Record => { + return decodeMessage(buf, Record.codec()) + } +} + +export interface Message { + type?: Message.MessageType + clusterLevelRaw?: number + key?: Uint8Array + record?: Uint8Array + closerPeers: Message.Peer[] + providerPeers: Message.Peer[] +} + +export namespace Message { + export enum MessageType { + PUT_VALUE = 'PUT_VALUE', + GET_VALUE = 'GET_VALUE', + ADD_PROVIDER = 'ADD_PROVIDER', + GET_PROVIDERS = 'GET_PROVIDERS', + FIND_NODE = 'FIND_NODE', + PING = 'PING' + } + + enum __MessageTypeValues { + PUT_VALUE = 0, + GET_VALUE = 1, + ADD_PROVIDER = 2, + GET_PROVIDERS = 3, + FIND_NODE = 4, + PING = 5 + } + + export namespace MessageType { + export const codec = (): Codec => { + return enumeration(__MessageTypeValues) + } + } + + export enum ConnectionType { + NOT_CONNECTED = 'NOT_CONNECTED', + CONNECTED = 'CONNECTED', + CAN_CONNECT = 'CAN_CONNECT', + CANNOT_CONNECT = 'CANNOT_CONNECT' + } + + enum __ConnectionTypeValues { + NOT_CONNECTED = 0, + CONNECTED = 1, + CAN_CONNECT = 2, + CANNOT_CONNECT = 3 + } + + export namespace ConnectionType { + export const codec = (): Codec => { + return enumeration(__ConnectionTypeValues) + } + } + + export interface Peer { + id?: Uint8Array + addrs: Uint8Array[] + connection?: Message.ConnectionType + } + + export namespace Peer { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.id != null) { + w.uint32(10) + w.bytes(obj.id) + } + + if (obj.addrs != null) { + for (const value of obj.addrs) { + w.uint32(18) + w.bytes(value) + } + } + + if (obj.connection != null) { + w.uint32(24) + Message.ConnectionType.codec().encode(obj.connection, w) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + addrs: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.id = reader.bytes() + break + case 2: + obj.addrs.push(reader.bytes()) + break + case 3: + obj.connection = Message.ConnectionType.codec().decode(reader) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer => { + return decodeMessage(buf, Peer.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.type != null) { + w.uint32(8) + Message.MessageType.codec().encode(obj.type, w) + } + + if (obj.clusterLevelRaw != null) { + w.uint32(80) + w.int32(obj.clusterLevelRaw) + } + + if (obj.key != null) { + w.uint32(18) + w.bytes(obj.key) + } + + if (obj.record != null) { + w.uint32(26) + w.bytes(obj.record) + } + + if (obj.closerPeers != null) { + for (const value of obj.closerPeers) { + w.uint32(66) + Message.Peer.codec().encode(value, w) + } + } + + if (obj.providerPeers != null) { + for (const value of obj.providerPeers) { + w.uint32(74) + Message.Peer.codec().encode(value, w) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + closerPeers: [], + providerPeers: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = Message.MessageType.codec().decode(reader) + break + case 10: + obj.clusterLevelRaw = reader.int32() + break + case 2: + obj.key = reader.bytes() + break + case 3: + obj.record = reader.bytes() + break + case 8: + obj.closerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) + break + case 9: + obj.providerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { + return decodeMessage(buf, Message.codec()) + } +} diff --git a/packages/kad-dht/src/message/index.ts b/packages/kad-dht/src/message/index.ts new file mode 100644 index 0000000000..ab946ab90c --- /dev/null +++ b/packages/kad-dht/src/message/index.ts @@ -0,0 +1,110 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { Libp2pRecord } from '@libp2p/record' +import { multiaddr } from '@multiformats/multiaddr' +import { Message as PBMessage } from './dht.js' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Uint8ArrayList } from 'uint8arraylist' + +export const MESSAGE_TYPE = PBMessage.MessageType +export const CONNECTION_TYPE = PBMessage.ConnectionType +export const MESSAGE_TYPE_LOOKUP = Object.keys(MESSAGE_TYPE) + +interface PBPeer { + id: Uint8Array + addrs: Uint8Array[] + connection: PBMessage.ConnectionType +} + +/** + * Represents a single DHT control message. + */ +export class Message { + public type: PBMessage.MessageType + public key: Uint8Array + private clusterLevelRaw: number + public closerPeers: PeerInfo[] + public providerPeers: PeerInfo[] + public record?: Libp2pRecord + + constructor (type: PBMessage.MessageType, key: Uint8Array, level: number) { + if (!(key instanceof Uint8Array)) { + throw new Error('Key must be a Uint8Array') + } + + this.type = type + this.key = key + this.clusterLevelRaw = level + this.closerPeers = [] + this.providerPeers = [] + this.record = undefined + } + + /** + * @type {number} + */ + get clusterLevel (): number { + const level = this.clusterLevelRaw - 1 + if (level < 0) { + return 0 + } + + return level + } + + set clusterLevel (level) { + this.clusterLevelRaw = level + } + + /** + * Encode into protobuf + */ + serialize (): Uint8Array { + return PBMessage.encode({ + key: this.key, + type: this.type, + clusterLevelRaw: this.clusterLevelRaw, + closerPeers: this.closerPeers.map(toPbPeer), + providerPeers: this.providerPeers.map(toPbPeer), + record: this.record == null ? undefined : this.record.serialize().subarray() + }) + } + + /** + * Decode from protobuf + */ + static deserialize (raw: Uint8ArrayList | Uint8Array): Message { + const dec = PBMessage.decode(raw) + + const msg = new Message(dec.type ?? PBMessage.MessageType.PUT_VALUE, dec.key ?? Uint8Array.from([]), dec.clusterLevelRaw ?? 0) + msg.closerPeers = dec.closerPeers.map(fromPbPeer) + msg.providerPeers = dec.providerPeers.map(fromPbPeer) + + if (dec.record?.length != null) { + msg.record = Libp2pRecord.deserialize(dec.record) + } + + return msg + } +} + +function toPbPeer (peer: PeerInfo): PBPeer { + const output: PBPeer = { + id: peer.id.toBytes(), + addrs: (peer.multiaddrs ?? []).map((m) => m.bytes), + connection: CONNECTION_TYPE.CONNECTED + } + + return output +} + +function fromPbPeer (peer: PBMessage.Peer): PeerInfo { + if (peer.id == null) { + throw new Error('Invalid peer in message') + } + + return { + id: peerIdFromBytes(peer.id), + multiaddrs: (peer.addrs ?? []).map((a) => multiaddr(a)), + protocols: [] + } +} diff --git a/packages/kad-dht/src/network.ts b/packages/kad-dht/src/network.ts new file mode 100644 index 0000000000..ca525af221 --- /dev/null +++ b/packages/kad-dht/src/network.ts @@ -0,0 +1,206 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { abortableDuplex } from 'abortable-iterator' +import drain from 'it-drain' +import first from 'it-first' +import * as lp from 'it-length-prefixed' +import { pipe } from 'it-pipe' +import { Message } from './message/index.js' +import { + dialPeerEvent, + sendQueryEvent, + peerResponseEvent, + queryErrorEvent +} from './query/events.js' +import type { KadDHTComponents, QueryEvent, QueryOptions } from './index.js' +import type { Stream } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Logger } from '@libp2p/logger' +import type { Duplex, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface NetworkInit { + protocol: string + lan: boolean +} + +interface NetworkEvents { + 'peer': CustomEvent +} + +/** + * Handle network operations for the dht + */ +export class Network extends EventEmitter implements Startable { + private readonly log: Logger + private readonly protocol: string + private running: boolean + private readonly components: KadDHTComponents + + /** + * Create a new network + */ + constructor (components: KadDHTComponents, init: NetworkInit) { + super() + + const { protocol, lan } = init + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:network`) + this.running = false + this.protocol = protocol + } + + /** + * Start the network + */ + async start (): Promise { + if (this.running) { + return + } + + this.running = true + } + + /** + * Stop all network activity + */ + async stop (): Promise { + this.running = false + } + + /** + * Is the network online? + */ + isStarted (): boolean { + return this.running + } + + /** + * Send a request and record RTT for latency measurements + */ + async * sendRequest (to: PeerId, msg: Message, options: QueryOptions = {}): AsyncGenerator { + if (!this.running) { + return + } + + this.log('sending %s to %p', msg.type, to) + yield dialPeerEvent({ peer: to }, options) + yield sendQueryEvent({ to, type: msg.type }, options) + + let stream: Stream | undefined + + try { + const connection = await this.components.connectionManager.openConnection(to, options) + const stream = await connection.newStream(this.protocol, options) + + const response = await this._writeReadMessage(stream, msg.serialize(), options) + + yield peerResponseEvent({ + from: to, + messageType: response.type, + closer: response.closerPeers, + providers: response.providerPeers, + record: response.record + }, options) + } catch (err: any) { + yield queryErrorEvent({ from: to, error: err }, options) + } finally { + if (stream != null) { + stream.close() + } + } + } + + /** + * Sends a message without expecting an answer + */ + async * sendMessage (to: PeerId, msg: Message, options: QueryOptions = {}): AsyncGenerator { + if (!this.running) { + return + } + + this.log('sending %s to %p', msg.type, to) + yield dialPeerEvent({ peer: to }, options) + yield sendQueryEvent({ to, type: msg.type }, options) + + let stream: Stream | undefined + + try { + const connection = await this.components.connectionManager.openConnection(to, options) + const stream = await connection.newStream(this.protocol, options) + + await this._writeMessage(stream, msg.serialize(), options) + + yield peerResponseEvent({ from: to, messageType: msg.type }, options) + } catch (err: any) { + yield queryErrorEvent({ from: to, error: err }, options) + } finally { + if (stream != null) { + stream.close() + } + } + } + + /** + * Write a message to the given stream + */ + async _writeMessage (stream: Duplex, Source>, msg: Uint8Array | Uint8ArrayList, options: AbortOptions): Promise { + if (options.signal != null) { + stream = abortableDuplex(stream, options.signal) + } + + await pipe( + [msg], + (source) => lp.encode(source), + stream, + drain + ) + } + + /** + * Write a message and read its response. + * If no response is received after the specified timeout + * this will error out. + */ + async _writeReadMessage (stream: Duplex, Source>, msg: Uint8Array | Uint8ArrayList, options: AbortOptions): Promise { + if (options.signal != null) { + stream = abortableDuplex(stream, options.signal) + } + + const res = await pipe( + [msg], + (source) => lp.encode(source), + stream, + (source) => lp.decode(source), + async source => { + const buf = await first(source) + + if (buf != null) { + return buf + } + + throw new CodeError('No message received', 'ERR_NO_MESSAGE_RECEIVED') + } + ) + + const message = Message.deserialize(res) + + // tell any listeners about new peers we've seen + message.closerPeers.forEach(peerData => { + this.dispatchEvent(new CustomEvent('peer', { + detail: peerData + })) + }) + message.providerPeers.forEach(peerData => { + this.dispatchEvent(new CustomEvent('peer', { + detail: peerData + })) + }) + + return message + } +} diff --git a/packages/kad-dht/src/peer-list/index.ts b/packages/kad-dht/src/peer-list/index.ts new file mode 100644 index 0000000000..7298a70c2b --- /dev/null +++ b/packages/kad-dht/src/peer-list/index.ts @@ -0,0 +1,54 @@ +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * A list of unique peers. + */ +export class PeerList { + private readonly list: PeerId[] + + constructor () { + this.list = [] + } + + /** + * Add a new peer. Returns `true` if it was a new one + */ + push (peerId: PeerId): boolean { + if (!this.has(peerId)) { + this.list.push(peerId) + + return true + } + + return false + } + + /** + * Check if this PeerInfo is already in here + */ + has (peerId: PeerId): boolean { + const match = this.list.find((i) => i.equals(peerId)) + return Boolean(match) + } + + /** + * Get the list as an array + */ + toArray (): PeerId[] { + return this.list.slice() + } + + /** + * Remove the last element + */ + pop (): PeerId | undefined { + return this.list.pop() + } + + /** + * The length of the list + */ + get length (): number { + return this.list.length + } +} diff --git a/packages/kad-dht/src/peer-list/peer-distance-list.ts b/packages/kad-dht/src/peer-list/peer-distance-list.ts new file mode 100644 index 0000000000..5cccd9a51d --- /dev/null +++ b/packages/kad-dht/src/peer-list/peer-distance-list.ts @@ -0,0 +1,92 @@ +import { compare as uint8ArrayCompare } from 'uint8arrays/compare' +import { xor as uint8ArrayXor } from 'uint8arrays/xor' +import * as utils from '../utils.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +interface PeerDistance { + peerId: PeerId + distance: Uint8Array +} + +/** + * Maintains a list of peerIds sorted by distance from a DHT key. + */ +export class PeerDistanceList { + /** + * The DHT key from which distance is calculated + */ + private readonly originDhtKey: Uint8Array + + /** + * The maximum size of the list + */ + private readonly capacity: number + + private peerDistances: PeerDistance[] + + constructor (originDhtKey: Uint8Array, capacity: number) { + this.originDhtKey = originDhtKey + this.capacity = capacity + this.peerDistances = [] + } + + /** + * The length of the list + */ + get length (): number { + return this.peerDistances.length + } + + /** + * The peerIds in the list, in order of distance from the origin key + */ + get peers (): PeerId[] { + return this.peerDistances.map(pd => pd.peerId) + } + + /** + * Add a peerId to the list. + */ + async add (peerId: PeerId): Promise { + if (this.peerDistances.find(pd => pd.peerId.equals(peerId)) != null) { + return + } + + const dhtKey = await utils.convertPeerId(peerId) + const el = { + peerId, + distance: uint8ArrayXor(this.originDhtKey, dhtKey) + } + + this.peerDistances.push(el) + this.peerDistances.sort((a, b) => uint8ArrayCompare(a.distance, b.distance)) + this.peerDistances = this.peerDistances.slice(0, this.capacity) + } + + /** + * Indicates whether any of the peerIds passed as a parameter are closer + * to the origin key than the furthest peerId in the PeerDistanceList. + */ + async anyCloser (peerIds: PeerId[]): Promise { + if (peerIds.length === 0) { + return false + } + + if (this.length === 0) { + return true + } + + const dhtKeys = await Promise.all(peerIds.map(utils.convertPeerId)) + const furthestDistance = this.peerDistances[this.peerDistances.length - 1].distance + + for (const dhtKey of dhtKeys) { + const keyDistance = uint8ArrayXor(this.originDhtKey, dhtKey) + + if (uint8ArrayCompare(keyDistance, furthestDistance) < 0) { + return true + } + } + + return false + } +} diff --git a/packages/kad-dht/src/peer-routing/index.ts b/packages/kad-dht/src/peer-routing/index.ts new file mode 100644 index 0000000000..ab0af91cb6 --- /dev/null +++ b/packages/kad-dht/src/peer-routing/index.ts @@ -0,0 +1,317 @@ +import { keys } from '@libp2p/crypto' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { Libp2pRecord } from '@libp2p/record' +import { verifyRecord } from '@libp2p/record/validators' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { PeerDistanceList } from '../peer-list/peer-distance-list.js' +import { + queryErrorEvent, + finalPeerEvent, + valueEvent +} from '../query/events.js' +import * as utils from '../utils.js' +import type { KadDHTComponents, DHTRecord, DialPeerEvent, FinalPeerEvent, QueryEvent, Validators } from '../index.js' +import type { Network } from '../network.js' +import type { QueryManager, QueryOptions } from '../query/manager.js' +import type { QueryFunc } from '../query/types.js' +import type { RoutingTable } from '../routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Logger } from '@libp2p/logger' + +export interface PeerRoutingInit { + routingTable: RoutingTable + network: Network + validators: Validators + queryManager: QueryManager + lan: boolean +} + +export class PeerRouting { + private readonly components: KadDHTComponents + private readonly log: Logger + private readonly routingTable: RoutingTable + private readonly network: Network + private readonly validators: Validators + private readonly queryManager: QueryManager + + constructor (components: KadDHTComponents, init: PeerRoutingInit) { + const { routingTable, network, validators, queryManager, lan } = init + + this.components = components + this.routingTable = routingTable + this.network = network + this.validators = validators + this.queryManager = queryManager + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:peer-routing`) + } + + /** + * Look if we are connected to a peer with the given id. + * Returns its id and addresses, if found, otherwise `undefined`. + */ + async findPeerLocal (peer: PeerId): Promise { + let peerData + const p = await this.routingTable.find(peer) + + if (p != null) { + this.log('findPeerLocal found %p in routing table', peer) + + try { + peerData = await this.components.peerStore.get(p) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + if (peerData == null) { + try { + peerData = await this.components.peerStore.get(peer) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + if (peerData != null) { + this.log('findPeerLocal found %p in peer store', peer) + + return { + id: peerData.id, + multiaddrs: peerData.addresses.map((address) => address.multiaddr), + protocols: [] + } + } + + return undefined + } + + /** + * Get a value via rpc call for the given parameters + */ + async * _getValueSingle (peer: PeerId, key: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + const msg = new Message(MESSAGE_TYPE.GET_VALUE, key, 0) + yield * this.network.sendRequest(peer, msg, options) + } + + /** + * Get the public key directly from a node + */ + async * getPublicKeyFromNode (peer: PeerId, options: AbortOptions = {}): AsyncGenerator { + const pkKey = utils.keyForPublicKey(peer) + + for await (const event of this._getValueSingle(peer, pkKey, options)) { + yield event + + if (event.name === 'PEER_RESPONSE' && event.record != null) { + const recPeer = await peerIdFromKeys(keys.marshalPublicKey({ bytes: event.record.value })) + + // compare hashes of the pub key + if (!recPeer.equals(peer)) { + throw new CodeError('public key does not match id', 'ERR_PUBLIC_KEY_DOES_NOT_MATCH_ID') + } + + if (recPeer.publicKey == null) { + throw new CodeError('public key missing', 'ERR_PUBLIC_KEY_MISSING') + } + + yield valueEvent({ from: peer, value: recPeer.publicKey }, options) + } + } + + throw new CodeError(`Node not responding with its public key: ${peer.toString()}`, 'ERR_INVALID_RECORD') + } + + /** + * Search for a peer with the given ID + */ + async * findPeer (id: PeerId, options: QueryOptions = {}): AsyncGenerator { + this.log('findPeer %p', id) + + // Try to find locally + const pi = await this.findPeerLocal(id) + + // already got it + if (pi != null) { + this.log('found local') + yield finalPeerEvent({ + from: this.components.peerId, + peer: pi + }, options) + return + } + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + const findPeerQuery: QueryFunc = async function * ({ peer, signal }) { + const request = new Message(MESSAGE_TYPE.FIND_NODE, id.toBytes(), 0) + + for await (const event of self.network.sendRequest(peer, request, { + ...options, + signal + })) { + yield event + + if (event.name === 'PEER_RESPONSE') { + const match = event.closer.find((p) => p.id.equals(id)) + + // found the peer + if (match != null) { + yield finalPeerEvent({ from: event.from, peer: match }, options) + } + } + } + } + + let foundPeer = false + + for await (const event of this.queryManager.run(id.toBytes(), findPeerQuery, options)) { + if (event.name === 'FINAL_PEER') { + foundPeer = true + } + + yield event + } + + if (!foundPeer) { + yield queryErrorEvent({ from: this.components.peerId, error: new CodeError('Not found', 'ERR_NOT_FOUND') }, options) + } + } + + /** + * Kademlia 'node lookup' operation on a key, which could be a the + * bytes from a multihash or a peer ID + */ + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + this.log('getClosestPeers to %b', key) + const id = await utils.convertBuffer(key) + const tablePeers = this.routingTable.closestPeers(id) + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + const peers = new PeerDistanceList(id, this.routingTable.kBucketSize) + await Promise.all(tablePeers.map(async peer => { await peers.add(peer) })) + + const getCloserPeersQuery: QueryFunc = async function * ({ peer, signal }) { + self.log('closerPeersSingle %s from %p', uint8ArrayToString(key, 'base32'), peer) + const request = new Message(MESSAGE_TYPE.FIND_NODE, key, 0) + + yield * self.network.sendRequest(peer, request, { + ...options, + signal + }) + } + + for await (const event of this.queryManager.run(key, getCloserPeersQuery, options)) { + yield event + + if (event.name === 'PEER_RESPONSE') { + await Promise.all(event.closer.map(async peerData => { await peers.add(peerData.id) })) + } + } + + this.log('found %d peers close to %b', peers.length, key) + + for (const peerId of peers.peers) { + try { + const peer = await this.components.peerStore.get(peerId) + + yield finalPeerEvent({ + from: this.components.peerId, + peer: { + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + } + }, options) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + } + + /** + * Query a particular peer for the value for the given key. + * It will either return the value or a list of closer peers. + * + * Note: The peerStore is updated with new addresses found for the given peer. + */ + async * getValueOrPeers (peer: PeerId, key: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + for await (const event of this._getValueSingle(peer, key, options)) { + if (event.name === 'PEER_RESPONSE') { + if (event.record != null) { + // We have a record + try { + await this._verifyRecordOnline(event.record) + } catch (err: any) { + const errMsg = 'invalid record received, discarded' + this.log(errMsg) + + yield queryErrorEvent({ from: event.from, error: new CodeError(errMsg, 'ERR_INVALID_RECORD') }, options) + continue + } + } + } + + yield event + } + } + + /** + * Verify a record, fetching missing public keys from the network. + * Throws an error if the record is invalid. + */ + async _verifyRecordOnline (record: DHTRecord): Promise { + if (record.timeReceived == null) { + throw new CodeError('invalid record received', 'ERR_INVALID_RECORD') + } + + await verifyRecord(this.validators, new Libp2pRecord(record.key, record.value, record.timeReceived)) + } + + /** + * Get the nearest peers to the given query, but if closer + * than self + */ + async getCloserPeersOffline (key: Uint8Array, closerThan: PeerId): Promise { + const id = await utils.convertBuffer(key) + const ids = this.routingTable.closestPeers(id) + const output: PeerInfo[] = [] + + for (const peerId of ids) { + if (peerId.equals(closerThan)) { + continue + } + + try { + const peer = await this.components.peerStore.get(peerId) + + output.push({ + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + }) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + if (output.length > 0) { + this.log('getCloserPeersOffline found %d peer(s) closer to %b than %p', output.length, key, closerThan) + } else { + this.log('getCloserPeersOffline could not find peer closer to %b than %p', key, closerThan) + } + + return output + } +} diff --git a/packages/kad-dht/src/providers.ts b/packages/kad-dht/src/providers.ts new file mode 100644 index 0000000000..46e2703792 --- /dev/null +++ b/packages/kad-dht/src/providers.ts @@ -0,0 +1,286 @@ +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import cache from 'hashlru' +import { Key } from 'interface-datastore/key' +import Queue from 'p-queue' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import varint from 'varint' +import { + PROVIDERS_CLEANUP_INTERVAL, + PROVIDERS_VALIDITY, + PROVIDERS_LRU_CACHE_SIZE, + PROVIDER_KEY_PREFIX +} from './constants.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Datastore } from 'interface-datastore' +import type { CID } from 'multiformats' + +const log = logger('libp2p:kad-dht:providers') + +export interface ProvidersInit { + cacheSize?: number + /** + * How often invalid records are cleaned. (in seconds) + */ + cleanupInterval?: number + /** + * How long is a provider valid for. (in seconds) + */ + provideValidity?: number +} + +export interface ProvidersComponents { + datastore: Datastore +} + +/** + * This class manages known providers. + * A provider is a peer that we know to have the content for a given CID. + * + * Every `cleanupInterval` providers are checked if they + * are still valid, i.e. younger than the `provideValidity`. + * If they are not, they are deleted. + * + * To ensure the list survives restarts of the daemon, + * providers are stored in the datastore, but to ensure + * access is fast there is an LRU cache in front of that. + */ +export class Providers implements Startable { + private readonly components: ProvidersComponents + private readonly cache: ReturnType + private readonly cleanupInterval: number + private readonly provideValidity: number + private readonly syncQueue: Queue + private started: boolean + private cleaner?: NodeJS.Timer + + constructor (components: ProvidersComponents, init: ProvidersInit = {}) { + const { cacheSize, cleanupInterval, provideValidity } = init + + this.components = components + this.cleanupInterval = cleanupInterval ?? PROVIDERS_CLEANUP_INTERVAL + this.provideValidity = provideValidity ?? PROVIDERS_VALIDITY + this.cache = cache(cacheSize ?? PROVIDERS_LRU_CACHE_SIZE) + this.syncQueue = new Queue({ concurrency: 1 }) + this.started = false + } + + isStarted (): boolean { + return this.started + } + + /** + * Start the provider cleanup service + */ + async start (): Promise { + if (this.started) { + return + } + + this.started = true + + this.cleaner = setInterval( + () => { + this._cleanup().catch(err => { + log.error(err) + }) + }, + this.cleanupInterval + ) + } + + /** + * Release any resources. + */ + async stop (): Promise { + this.started = false + + if (this.cleaner != null) { + clearInterval(this.cleaner) + this.cleaner = undefined + } + } + + /** + * Check all providers if they are still valid, and if not delete them + */ + async _cleanup (): Promise { + await this.syncQueue.add(async () => { + const start = Date.now() + + let count = 0 + let deleteCount = 0 + const deleted = new Map>() + const batch = this.components.datastore.batch() + + // Get all provider entries from the datastore + const query = this.components.datastore.query({ prefix: PROVIDER_KEY_PREFIX }) + + for await (const entry of query) { + try { + // Add a delete to the batch for each expired entry + const { cid, peerId } = parseProviderKey(entry.key) + const time = readTime(entry.value).getTime() + const now = Date.now() + const delta = now - time + const expired = delta > this.provideValidity + + log('comparing: %d - %d = %d > %d %s', now, time, delta, this.provideValidity, expired ? '(expired)' : '') + + if (expired) { + deleteCount++ + batch.delete(entry.key) + const peers = deleted.get(cid) ?? new Set() + peers.add(peerId) + deleted.set(cid, peers) + } + count++ + } catch (err: any) { + log.error(err.message) + } + } + + // Commit the deletes to the datastore + if (deleted.size > 0) { + log('deleting %d / %d entries', deleteCount, count) + await batch.commit() + } else { + log('nothing to delete') + } + + // Clear expired entries from the cache + for (const [cid, peers] of deleted) { + const key = makeProviderKey(cid) + const provs = this.cache.get(key) + + if (provs != null) { + for (const peerId of peers) { + provs.delete(peerId) + } + + if (provs.size === 0) { + this.cache.remove(key) + } else { + this.cache.set(key, provs) + } + } + } + + log('Cleanup successful (%dms)', Date.now() - start) + }) + } + + /** + * Get the currently known provider peer ids for a given CID + */ + async _getProvidersMap (cid: CID): Promise> { + const cacheKey = makeProviderKey(cid) + let provs: Map = this.cache.get(cacheKey) + + if (provs == null) { + provs = await loadProviders(this.components.datastore, cid) + this.cache.set(cacheKey, provs) + } + + return provs + } + + /** + * Add a new provider for the given CID + */ + async addProvider (cid: CID, provider: PeerId): Promise { + await this.syncQueue.add(async () => { + log('%p provides %s', provider, cid) + const provs = await this._getProvidersMap(cid) + + log('loaded %s provs', provs.size) + const now = new Date() + provs.set(provider.toString(), now) + + const dsKey = makeProviderKey(cid) + this.cache.set(dsKey, provs) + + await writeProviderEntry(this.components.datastore, cid, provider, now) + }) + } + + /** + * Get a list of providers for the given CID + */ + async getProviders (cid: CID): Promise { + return this.syncQueue.add(async () => { + log('get providers for %s', cid) + const provs = await this._getProvidersMap(cid) + + return [...provs.keys()].map(peerIdStr => { + return peerIdFromString(peerIdStr) + }) + }, { + // no timeout is specified for this queue so it will not + // throw, but this is required to get the right return + // type since p-queue@7.3.4 + throwOnTimeout: true + }) + } +} + +/** + * Encode the given key its matching datastore key + */ +function makeProviderKey (cid: CID | string): string { + const cidStr = typeof cid === 'string' ? cid : uint8ArrayToString(cid.multihash.bytes, 'base32') + + return `${PROVIDER_KEY_PREFIX}/${cidStr}` +} + +/** + * Write a provider into the given store + */ +async function writeProviderEntry (store: Datastore, cid: CID, peer: PeerId, time: Date): Promise { + const dsKey = [ + makeProviderKey(cid), + '/', + peer.toString() + ].join('') + + const key = new Key(dsKey) + const buffer = Uint8Array.from(varint.encode(time.getTime())) + + await store.put(key, buffer) +} + +/** + * Parse the CID and provider peer id from the key + */ +function parseProviderKey (key: Key): { cid: string, peerId: string } { + const parts = key.toString().split('/') + + if (parts.length !== 5) { + throw new Error(`incorrectly formatted provider entry key in datastore: ${key.toString()}`) + } + + return { + cid: parts[3], + peerId: parts[4] + } +} + +/** + * Load providers for the given CID from the store + */ +async function loadProviders (store: Datastore, cid: CID): Promise> { + const providers = new Map() + const query = store.query({ prefix: makeProviderKey(cid) }) + + for await (const entry of query) { + const { peerId } = parseProviderKey(entry.key) + providers.set(peerId, readTime(entry.value)) + } + + return providers +} + +function readTime (buf: Uint8Array): Date { + return new Date(varint.decode(buf)) +} diff --git a/packages/kad-dht/src/query-self.ts b/packages/kad-dht/src/query-self.ts new file mode 100644 index 0000000000..300a364c02 --- /dev/null +++ b/packages/kad-dht/src/query-self.ts @@ -0,0 +1,163 @@ +import { setMaxListeners } from 'events' +import { logger, type Logger } from '@libp2p/logger' +import { anySignal } from 'any-signal' +import length from 'it-length' +import { pipe } from 'it-pipe' +import take from 'it-take' +import pDefer from 'p-defer' +import { pEvent } from 'p-event' +import { QUERY_SELF_INTERVAL, QUERY_SELF_TIMEOUT, K, QUERY_SELF_INITIAL_INTERVAL } from './constants.js' +import type { PeerRouting } from './peer-routing/index.js' +import type { RoutingTable } from './routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { DeferredPromise } from 'p-defer' + +export interface QuerySelfInit { + lan: boolean + peerRouting: PeerRouting + routingTable: RoutingTable + count?: number + interval?: number + initialInterval?: number + queryTimeout?: number + initialQuerySelfHasRun: DeferredPromise +} + +export interface QuerySelfComponents { + peerId: PeerId +} + +/** + * Receives notifications of new peers joining the network that support the DHT protocol + */ +export class QuerySelf implements Startable { + private readonly log: Logger + private readonly components: QuerySelfComponents + private readonly peerRouting: PeerRouting + private readonly routingTable: RoutingTable + private readonly count: number + private readonly interval: number + private readonly initialInterval: number + private readonly queryTimeout: number + private started: boolean + private timeoutId?: NodeJS.Timer + private controller?: AbortController + private initialQuerySelfHasRun?: DeferredPromise + private querySelfPromise?: DeferredPromise + + constructor (components: QuerySelfComponents, init: QuerySelfInit) { + const { peerRouting, lan, count, interval, queryTimeout, routingTable } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:query-self`) + this.started = false + this.peerRouting = peerRouting + this.routingTable = routingTable + this.count = count ?? K + this.interval = interval ?? QUERY_SELF_INTERVAL + this.initialInterval = init.initialInterval ?? QUERY_SELF_INITIAL_INTERVAL + this.queryTimeout = queryTimeout ?? QUERY_SELF_TIMEOUT + this.initialQuerySelfHasRun = init.initialQuerySelfHasRun + } + + isStarted (): boolean { + return this.started + } + + start (): void { + if (this.started) { + return + } + + this.started = true + clearTimeout(this.timeoutId) + this.timeoutId = setTimeout(() => { + this.querySelf() + .catch(err => { + this.log.error('error running self-query', err) + }) + }, this.initialInterval) + } + + stop (): void { + this.started = false + + if (this.timeoutId != null) { + clearTimeout(this.timeoutId) + } + + if (this.controller != null) { + this.controller.abort() + } + } + + async querySelf (): Promise { + if (!this.started) { + this.log('skip self-query because we are not started') + return + } + + if (this.querySelfPromise != null) { + this.log('joining existing self query') + return this.querySelfPromise.promise + } + + this.querySelfPromise = pDefer() + + if (this.routingTable.size === 0) { + // wait to discover at least one DHT peer + await pEvent(this.routingTable, 'peer:add') + } + + if (this.started) { + this.controller = new AbortController() + const signal = anySignal([this.controller.signal, AbortSignal.timeout(this.queryTimeout)]) + + // this controller will get used for lots of dial attempts so make sure we don't cause warnings to be logged + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, signal) + } + } catch {} // fails on node < 15.4 + + try { + this.log('run self-query, look for %d peers timing out after %dms', this.count, this.queryTimeout) + + const found = await pipe( + this.peerRouting.getClosestPeers(this.components.peerId.toBytes(), { + signal, + isSelfQuery: true + }), + (source) => take(source, this.count), + async (source) => length(source) + ) + + this.log('self-query ran successfully - found %d peers', found) + + if (this.initialQuerySelfHasRun != null) { + this.initialQuerySelfHasRun.resolve() + this.initialQuerySelfHasRun = undefined + } + } catch (err: any) { + this.log.error('self-query error', err) + } finally { + signal.clear() + } + } + + this.querySelfPromise.resolve() + this.querySelfPromise = undefined + + if (!this.started) { + return + } + + this.timeoutId = setTimeout(() => { + this.querySelf() + .catch(err => { + this.log.error('error running self-query', err) + }) + }, this.interval) + } +} diff --git a/packages/kad-dht/src/query/events.ts b/packages/kad-dht/src/query/events.ts new file mode 100644 index 0000000000..d838e2b47e --- /dev/null +++ b/packages/kad-dht/src/query/events.ts @@ -0,0 +1,149 @@ +import { CustomEvent } from '@libp2p/interfaces/events' +import { MESSAGE_TYPE_LOOKUP } from '../message/index.js' +import type { SendQueryEvent, PeerResponseEvent, DialPeerEvent, AddPeerEvent, ValueEvent, ProviderEvent, QueryErrorEvent, FinalPeerEvent, QueryOptions } from '../index.js' +import type { Message } from '../message/dht.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Libp2pRecord } from '@libp2p/record' + +export interface QueryEventFields { + to: PeerId + type: Message.MessageType +} + +export function sendQueryEvent (fields: QueryEventFields, options: QueryOptions = {}): SendQueryEvent { + const event: SendQueryEvent = { + ...fields, + name: 'SEND_QUERY', + type: 0, + messageName: fields.type, + messageType: MESSAGE_TYPE_LOOKUP.indexOf(fields.type.toString()) + } + + options.onProgress?.(new CustomEvent('kad-dht:query:send-query', { detail: event })) + + return event +} + +export interface PeerResponseEventField { + from: PeerId + messageType: Message.MessageType + closer?: PeerInfo[] + providers?: PeerInfo[] + record?: Libp2pRecord +} + +export function peerResponseEvent (fields: PeerResponseEventField, options: QueryOptions = {}): PeerResponseEvent { + const event: PeerResponseEvent = { + ...fields, + name: 'PEER_RESPONSE', + type: 1, + messageName: fields.messageType, + closer: (fields.closer != null) ? fields.closer : [], + providers: (fields.providers != null) ? fields.providers : [] + } + + options.onProgress?.(new CustomEvent('kad-dht:query:peer-response', { detail: event })) + + return event +} + +export interface FinalPeerEventFields { + from: PeerId + peer: PeerInfo +} + +export function finalPeerEvent (fields: FinalPeerEventFields, options: QueryOptions = {}): FinalPeerEvent { + const event: FinalPeerEvent = { + ...fields, + name: 'FINAL_PEER', + type: 2 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:final-peer', { detail: event })) + + return event +} + +export interface ErrorEventFields { + from: PeerId + error: Error +} + +export function queryErrorEvent (fields: ErrorEventFields, options: QueryOptions = {}): QueryErrorEvent { + const event: QueryErrorEvent = { + ...fields, + name: 'QUERY_ERROR', + type: 3 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:query-error', { detail: event })) + + return event +} + +export interface ProviderEventFields { + from: PeerId + providers: PeerInfo[] +} + +export function providerEvent (fields: ProviderEventFields, options: QueryOptions = {}): ProviderEvent { + const event: ProviderEvent = { + ...fields, + name: 'PROVIDER', + type: 4 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:provider', { detail: event })) + + return event +} + +export interface ValueEventFields { + from: PeerId + value: Uint8Array +} + +export function valueEvent (fields: ValueEventFields, options: QueryOptions = {}): ValueEvent { + const event: ValueEvent = { + ...fields, + name: 'VALUE', + type: 5 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:value', { detail: event })) + + return event +} + +export interface PeerEventFields { + peer: PeerId +} + +export function addPeerEvent (fields: PeerEventFields, options: QueryOptions = {}): AddPeerEvent { + const event: AddPeerEvent = { + ...fields, + name: 'ADD_PEER', + type: 6 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:add-peer', { detail: event })) + + return event +} + +export interface DialPeerEventFields { + peer: PeerId +} + +export function dialPeerEvent (fields: DialPeerEventFields, options: QueryOptions = {}): DialPeerEvent { + const event: DialPeerEvent = { + ...fields, + name: 'DIAL_PEER', + type: 7 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:dial-peer', { detail: event })) + + return event +} diff --git a/packages/kad-dht/src/query/manager.ts b/packages/kad-dht/src/query/manager.ts new file mode 100644 index 0000000000..9eb2fe0210 --- /dev/null +++ b/packages/kad-dht/src/query/manager.ts @@ -0,0 +1,227 @@ +import { setMaxListeners } from 'events' +import { AbortError } from '@libp2p/interfaces/errors' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { PeerSet } from '@libp2p/peer-collections' +import { anySignal } from 'any-signal' +import merge from 'it-merge' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { + ALPHA, K, DEFAULT_QUERY_TIMEOUT +} from '../constants.js' +import { convertBuffer } from '../utils.js' +import { queryPath } from './query-path.js' +import type { QueryFunc } from './types.js' +import type { QueryEvent, QueryOptions as RootQueryOptions } from '../index.js' +import type { RoutingTable } from '../routing-table/index.js' +import type { Metric, Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { DeferredPromise } from 'p-defer' + +export interface CleanUpEvents { + 'cleanup': CustomEvent +} + +export interface QueryManagerInit { + lan?: boolean + disjointPaths?: number + alpha?: number + initialQuerySelfHasRun: DeferredPromise + routingTable: RoutingTable +} + +export interface QueryManagerComponents { + peerId: PeerId + metrics?: Metrics +} + +export interface QueryOptions extends RootQueryOptions { + queryFuncTimeout?: number + isSelfQuery?: boolean +} + +/** + * Keeps track of all running queries + */ +export class QueryManager implements Startable { + private readonly components: QueryManagerComponents + private readonly lan: boolean + public disjointPaths: number + private readonly alpha: number + private readonly shutDownController: AbortController + private running: boolean + private queries: number + private metrics?: { + runningQueries: Metric + queryTime: Metric + } + + private readonly routingTable: RoutingTable + private initialQuerySelfHasRun?: DeferredPromise + + constructor (components: QueryManagerComponents, init: QueryManagerInit) { + const { lan = false, disjointPaths = K, alpha = ALPHA } = init + + this.components = components + this.disjointPaths = disjointPaths ?? K + this.running = false + this.alpha = alpha ?? ALPHA + this.lan = lan + this.queries = 0 + this.initialQuerySelfHasRun = init.initialQuerySelfHasRun + this.routingTable = init.routingTable + + // allow us to stop queries on shut down + this.shutDownController = new AbortController() + // make sure we don't make a lot of noise in the logs + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, this.shutDownController.signal) + } + } catch {} // fails on node < 15.4 + } + + isStarted (): boolean { + return this.running + } + + /** + * Starts the query manager + */ + async start (): Promise { + this.running = true + + if (this.components.metrics != null && this.metrics == null) { + this.metrics = { + runningQueries: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_running_queries`), + queryTime: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_query_time_seconds`) + } + } + } + + /** + * Stops all queries + */ + async stop (): Promise { + this.running = false + + this.shutDownController.abort() + } + + async * run (key: Uint8Array, queryFunc: QueryFunc, options: QueryOptions = {}): AsyncGenerator { + if (!this.running) { + throw new Error('QueryManager not started') + } + + const stopQueryTimer = this.metrics?.queryTime.timer() + + if (options.signal == null) { + // don't let queries run forever + options.signal = AbortSignal.timeout(DEFAULT_QUERY_TIMEOUT) + + // this signal will get listened to for network requests, etc + // so make sure we don't make a lot of noise in the logs + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, options.signal) + } + } catch {} // fails on node < 15.4 + } + + const signal = anySignal([this.shutDownController.signal, options.signal]) + + // this signal will get listened to for every invocation of queryFunc + // so make sure we don't make a lot of noise in the logs + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, signal) + } + } catch {} // fails on node < 15.4 + + const log = logger(`libp2p:kad-dht:${this.lan ? 'lan' : 'wan'}:query:` + uint8ArrayToString(key, 'base58btc')) + + // query a subset of peers up to `kBucketSize / 2` in length + const startTime = Date.now() + const cleanUp = new EventEmitter() + + try { + if (options.isSelfQuery !== true && this.initialQuerySelfHasRun != null) { + log('waiting for initial query-self query before continuing') + + await Promise.race([ + new Promise((resolve, reject) => { + signal.addEventListener('abort', () => { + reject(new AbortError('Query was aborted before self-query ran')) + }) + }), + this.initialQuerySelfHasRun.promise + ]) + + this.initialQuerySelfHasRun = undefined + } + + log('query:start') + this.queries++ + this.metrics?.runningQueries.update(this.queries) + + const id = await convertBuffer(key) + const peers = this.routingTable.closestPeers(id) + const peersToQuery = peers.slice(0, Math.min(this.disjointPaths, peers.length)) + + if (peers.length === 0) { + log.error('Running query with no peers') + return + } + + // make sure we don't get trapped in a loop + const peersSeen = new PeerSet() + + // Create query paths from the starting peers + const paths = peersToQuery.map((peer, index) => { + return queryPath({ + key, + startingPeer: peer, + ourPeerId: this.components.peerId, + signal, + query: queryFunc, + pathIndex: index, + numPaths: peersToQuery.length, + alpha: this.alpha, + cleanUp, + queryFuncTimeout: options.queryFuncTimeout, + log, + peersSeen, + onProgress: options.onProgress + }) + }) + + // Execute the query along each disjoint path and yield their results as they become available + for await (const event of merge(...paths)) { + yield event + + if (event.name === 'QUERY_ERROR') { + log('error', event.error) + } + } + } catch (err: any) { + if (!this.running && err.code === 'ERR_QUERY_ABORTED') { + // ignore query aborted errors that were thrown during query manager shutdown + } else { + throw err + } + } finally { + signal.clear() + + this.queries-- + this.metrics?.runningQueries.update(this.queries) + + if (stopQueryTimer != null) { + stopQueryTimer() + } + + cleanUp.dispatchEvent(new CustomEvent('cleanup')) + log('query:done in %dms', Date.now() - startTime) + } + } +} diff --git a/packages/kad-dht/src/query/query-path.ts b/packages/kad-dht/src/query/query-path.ts new file mode 100644 index 0000000000..2966b2947d --- /dev/null +++ b/packages/kad-dht/src/query/query-path.ts @@ -0,0 +1,254 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { anySignal } from 'any-signal' +import defer from 'p-defer' +import Queue from 'p-queue' +import { toString } from 'uint8arrays/to-string' +import { xor } from 'uint8arrays/xor' +import { convertPeerId, convertBuffer } from '../utils.js' +import { queryErrorEvent } from './events.js' +import type { CleanUpEvents } from './manager.js' +import type { QueryEvent, QueryOptions } from '../index.js' +import type { QueryFunc } from '../query/types.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { EventEmitter } from '@libp2p/interfaces/events' +import type { Logger } from '@libp2p/logger' +import type { PeerSet } from '@libp2p/peer-collections' + +const MAX_XOR = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') + +export interface QueryPathOptions extends QueryOptions { + /** + * What are we trying to find + */ + key: Uint8Array + + /** + * Where we start our query + */ + startingPeer: PeerId + + /** + * Who we are + */ + ourPeerId: PeerId + + /** + * When to stop querying + */ + signal: AbortSignal + + /** + * The query function to run with each peer + */ + query: QueryFunc + + /** + * How many concurrent node/value lookups to run + */ + alpha: number + + /** + * How many concurrent node/value lookups to run + */ + pathIndex: number + + /** + * How many concurrent node/value lookups to run + */ + numPaths: number + + /** + * will emit a 'cleanup' event if the caller exits the for..await of early + */ + cleanUp: EventEmitter + + /** + * A timeout for queryFunc in ms + */ + queryFuncTimeout?: number + + /** + * Query log + */ + log: Logger + + /** + * Set of peers seen by this and other paths + */ + peersSeen: PeerSet +} + +/** + * Walks a path through the DHT, calling the passed query function for + * every peer encountered that we have not seen before + */ +export async function * queryPath (options: QueryPathOptions): AsyncGenerator { + const { key, startingPeer, ourPeerId, signal, query, alpha, pathIndex, numPaths, cleanUp, queryFuncTimeout, log, peersSeen } = options + // Only ALPHA node/value lookups are allowed at any given time for each process + // https://github.com/libp2p/specs/tree/master/kad-dht#alpha-concurrency-parameter-%CE%B1 + const queue = new Queue({ + concurrency: alpha + }) + + // perform lookups on kadId, not the actual value + const kadId = await convertBuffer(key) + + /** + * Adds the passed peer to the query queue if it's not us and no + * other path has passed through this peer + */ + function queryPeer (peer: PeerId, peerKadId: Uint8Array): void { + if (peer == null) { + return + } + + peersSeen.add(peer) + + const peerXor = BigInt('0x' + toString(xor(peerKadId, kadId), 'base16')) + + queue.add(async () => { + const signals = [signal] + + if (queryFuncTimeout != null) { + signals.push(AbortSignal.timeout(queryFuncTimeout)) + } + + const compoundSignal = anySignal(signals) + + try { + for await (const event of query({ + key, + peer, + signal: compoundSignal, + pathIndex, + numPaths + })) { + if (compoundSignal.aborted) { + return + } + + // if there are closer peers and the query has not completed, continue the query + if (event.name === 'PEER_RESPONSE') { + for (const closerPeer of event.closer) { + if (peersSeen.has(closerPeer.id)) { // eslint-disable-line max-depth + log('already seen %p in query', closerPeer.id) + continue + } + + if (ourPeerId.equals(closerPeer.id)) { // eslint-disable-line max-depth + log('not querying ourselves') + continue + } + + const closerPeerKadId = await convertPeerId(closerPeer.id) + const closerPeerXor = BigInt('0x' + toString(xor(closerPeerKadId, kadId), 'base16')) + + // only continue query if closer peer is actually closer + if (closerPeerXor > peerXor) { // eslint-disable-line max-depth + log('skipping %p as they are not closer to %b than %p', closerPeer.id, key, peer) + continue + } + + log('querying closer peer %p', closerPeer.id) + queryPeer(closerPeer.id, closerPeerKadId) + } + } + queue.emit('completed', event) + } + } catch (err: any) { + if (!signal.aborted) { + return queryErrorEvent({ + from: peer, + error: err + }, options) + } + } finally { + compoundSignal.clear() + } + }, { + // use xor value as the queue priority - closer peers should execute first + // subtract it from MAX_XOR because higher priority values execute sooner + + // @ts-expect-error this is supposed to be a Number but it's ok to use BigInts + // as long as all priorities are BigInts since we won't mix BigInts and Number + // values in arithmetic operations + priority: MAX_XOR - peerXor + }).catch(err => { + log.error(err) + }) + } + + // begin the query with the starting peer + queryPeer(startingPeer, await convertPeerId(startingPeer)) + + // yield results as they come in + yield * toGenerator(queue, signal, cleanUp, log) +} + +async function * toGenerator (queue: Queue, signal: AbortSignal, cleanUp: EventEmitter, log: Logger): AsyncGenerator { + let deferred = defer() + let running = true + const results: QueryEvent[] = [] + + const cleanup = (): void => { + if (!running) { + return + } + + log('clean up queue, results %d, queue size %d, pending tasks %d', results.length, queue.size, queue.pending) + + running = false + queue.clear() + results.splice(0, results.length) + } + + queue.on('completed', result => { + results.push(result) + deferred.resolve() + }) + queue.on('error', err => { + log('queue error', err) + cleanup() + deferred.reject(err) + }) + queue.on('idle', () => { + log('queue idle') + running = false + deferred.resolve() + }) + + // clear the queue and throw if the query is aborted + signal.addEventListener('abort', () => { + log('abort queue') + const wasRunning = running + cleanup() + + if (wasRunning) { + deferred.reject(new CodeError('Query aborted', 'ERR_QUERY_ABORTED')) + } + }) + + // the user broke out of the loop early, ensure we resolve the deferred result + // promise and clear the queue of any remaining jobs + cleanUp.addEventListener('cleanup', () => { + cleanup() + deferred.resolve() + }) + + while (running) { // eslint-disable-line no-unmodified-loop-condition + await deferred.promise + deferred = defer() + + // yield all available results + while (results.length > 0) { + const result = results.shift() + + if (result != null) { + yield result + } + } + } + + // yield any remaining results + yield * results +} diff --git a/packages/kad-dht/src/query/types.ts b/packages/kad-dht/src/query/types.ts new file mode 100644 index 0000000000..6c43d4ba72 --- /dev/null +++ b/packages/kad-dht/src/query/types.ts @@ -0,0 +1,22 @@ +import type { QueryEvent } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +export interface QueryContext { + // the key we are looking up + key: Uint8Array + // the current peer being queried + peer: PeerId + // if this signal emits an 'abort' event, any long-lived processes or requests started as part of this query should be terminated + signal: AbortSignal + // which disjoint path we are following + pathIndex: number + // the total number of disjoint paths being executed + numPaths: number +} + +/** + * Query function + */ +export interface QueryFunc { + (context: QueryContext): AsyncIterable +} diff --git a/packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts b/packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts new file mode 100644 index 0000000000..1f667e07db --- /dev/null +++ b/packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts @@ -0,0 +1,1026 @@ +export default [ + 77591, 22417, 43971, 28421, 740, 29829, 71467, 228973, 196661, 78537, 27689, 36431, 44415, 14362, 19456, 106025, + 96308, 2882, 49509, 21149, 87173, 131409, 75844, 23676, 121838, 30291, 17492, 2953, 7564, 110620, 129477, 127283, + 53113, 72417, 165166, 109690, 21200, 102125, 24049, 71504, 90342, 25307, 72039, 26812, 26715, 32264, 133800, 71161, + 88956, 171987, 51779, 24425, 16671, 30251, 186294, 247761, 14202, 2121, 8465, 35024, 4876, 85917, 169730, 3638, + 256836, 96184, 943, 18678, 6583, 52907, 35807, 112254, 214097, 18796, 11595, 9243, 23554, 887, 268203, 382004, + 24590, 111335, 11625, 16619, 29039, 102425, 69006, 97976, 92362, 32552, 63717, 41433, 128974, 137630, 59943, 10019, + 13986, 35430, 33665, 108037, 43799, 43280, 38195, 29078, 58629, 18265, 14425, 46832, 235538, 40830, 77881, 110717, + 58937, 3463, 325358, 51300, 47623, 117252, 19007, 10170, 20540, 91237, 294813, 4951, 79841, 56232, 36270, 128547, + 69209, 66275, 100156, 32063, 73531, 34439, 80937, 28892, 44466, 88595, 216307, 32583, 49620, 16605, 82127, 45807, + 21630, 78726, 20235, 40163, 111007, 96926, 5567, 72083, 21665, 58844, 39419, 179767, 48328, 42662, 51550, 5251, + 37811, 49608, 81056, 50854, 55513, 20922, 18891, 197409, 164656, 32593, 71449, 220474, 58919, 85682, 67854, 13758, + 35066, 3565, 61905, 214793, 119572, 141419, 21504, 10302, 27354, 67003, 46131, 32668, 15165, 64871, 34450, 17821, + 2757, 11452, 34189, 5160, 12257, 85523, 560, 53385, 65887, 119549, 135620, 312353, 115979, 122356, 10867, 193231, + 124537, 54783, 90675, 120791, 4715, 142253, 50943, 17271, 43358, 25331, 4917, 120566, 34580, 12878, 33786, 160528, + 32523, 4869, 301307, 104817, 81491, 23276, 8832, 97911, 31265, 52065, 7998, 49622, 9715, 43998, 34091, 84587, + 20664, 69041, 29419, 53205, 10838, 58288, 116145, 6185, 5154, 141795, 35924, 21307, 144738, 43730, 12085, 8279, + 10002, 119, 133779, 199668, 72938, 31768, 39176, 67875, 38453, 9700, 44144, 4121, 116048, 41733, 12868, 82669, + 92308, 128, 34262, 11332, 7712, 90764, 36141, 13553, 71312, 77470, 117314, 96549, 49135, 23602, 54468, 28605, + 6327, 62308, 17171, 67531, 21319, 14105, 894, 107722, 46157, 8503, 51069, 100472, 45138, 15246, 14577, 35609, + 191464, 1757, 13364, 161349, 32067, 91705, 81144, 52339, 5408, 91066, 21983, 14157, 100545, 4372, 26630, 129112, + 1423, 29676, 213626, 4397, 88436, 99190, 6877, 49958, 26122, 114348, 60661, 29818, 293118, 50042, 179738, 16400, + 163423, 89627, 31040, 43973, 36638, 45952, 5153, 1894, 109322, 1898, 134021, 12402, 112077, 68309, 190269, 69866, + 31938, 107383, 11522, 105232, 11248, 14868, 39852, 71707, 186525, 16530, 38162, 106212, 11700, 5130, 16608, 26998, + 59586, 108399, 230033, 43683, 48135, 82179, 2073, 5015, 196684, 189293, 16378, 23452, 8301, 35640, 11632, 214551, + 29240, 57644, 33137, 91949, 55157, 52384, 117313, 5090, 17717, 89668, 49363, 82238, 241035, 66216, 29066, 184088, + 97206, 62820, 26595, 4241, 135635, 173672, 8202, 459, 71355, 146294, 29587, 3008, 135385, 141203, 14803, 6634, + 45094, 69362, 50925, 546, 51884, 62011, 83296, 234584, 44515, 56050, 89476, 87751, 19373, 12691, 149923, 19794, + 13833, 35846, 87557, 58339, 2884, 19145, 25647, 12224, 11024, 77338, 64608, 122297, 53025, 7205, 36189, 36294, + 170779, 21750, 7739, 173883, 75192, 35664, 224240, 113121, 30181, 26267, 27036, 117827, 92015, 106516, 55628, 203549, + 67949, 60462, 60844, 35911, 20457, 1820, 920, 19773, 8738, 73173, 181993, 38521, 98254, 76257, 46008, 92796, + 5384, 26868, 151566, 22124, 2411, 15919, 186872, 180021, 28099, 152961, 78811, 80237, 62352, 102653, 74259, 184890, + 16792, 123702, 224945, 29940, 19512, 75283, 14059, 112691, 92811, 233329, 20411, 138569, 53341, 109802, 50600, 134528, + 66747, 5529, 166531, 31578, 64732, 67189, 1596, 126357, 967, 167999, 206598, 109752, 119431, 207825, 78791, 91938, + 10301, 27311, 24233, 252343, 28831, 32812, 66002, 112267, 90895, 8786, 8095, 16824, 22866, 21813, 60507, 174833, + 19549, 130985, 117051, 52110, 6938, 81923, 123864, 38061, 919, 18680, 53534, 46739, 112893, 161529, 85429, 26761, + 11900, 81121, 91968, 15390, 217947, 56524, 1713, 6654, 37089, 85630, 138866, 61850, 16491, 75577, 16884, 98296, + 73523, 6140, 44645, 6062, 36366, 29844, 57946, 37932, 42472, 5266, 20834, 19309, 33753, 127182, 134259, 35810, + 41805, 45878, 312001, 14881, 47757, 49251, 120050, 44252, 3708, 25856, 107864, 120347, 1228, 36550, 41682, 34496, + 47025, 8393, 173365, 246526, 12894, 161607, 35670, 90785, 126572, 2095, 124731, 157033, 58694, 554, 12786, 9642, + 4817, 16136, 47864, 174698, 66992, 4639, 69284, 10625, 40710, 27763, 51738, 30404, 264105, 137904, 109882, 52487, + 42824, 57514, 2740, 10479, 146799, 107390, 16586, 88038, 174951, 9410, 16185, 44158, 5568, 40658, 46108, 12763, + 97385, 26175, 108859, 664, 230732, 67470, 46663, 14395, 50750, 141320, 93140, 15361, 47997, 55784, 6791, 307840, + 118569, 107326, 18056, 58281, 260415, 54691, 8790, 73332, 45633, 7511, 45674, 143373, 14031, 11799, 94491, 35646, + 96544, 14560, 26049, 32983, 25791, 83814, 42094, 231370, 63955, 139212, 2359, 169908, 3108, 183486, 105867, 28197, + 32941, 124968, 26402, 88267, 149768, 23053, 3078, 19091, 52924, 25383, 19209, 111548, 97361, 3959, 24880, 235061, + 9099, 24921, 161254, 151405, 20508, 7159, 34381, 20133, 11434, 74036, 19974, 34769, 36585, 1076, 22454, 17354, + 38727, 235160, 111547, 96454, 117448, 156940, 91330, 37299, 7310, 26915, 117060, 51369, 22620, 61861, 322264, 106850, + 111694, 15091, 2624, 40345, 300446, 177064, 1707, 27389, 54792, 327783, 132669, 183543, 59003, 17744, 20603, 151134, + 106923, 53084, 71803, 279424, 319816, 11579, 21946, 16728, 38274, 72711, 5085, 83391, 88646, 40159, 25027, 34680, + 10752, 12988, 54126, 30365, 18338, 100445, 230674, 44874, 84974, 143877, 123253, 139372, 28082, 91477, 144002, 13096, + 219729, 46016, 50029, 42377, 14601, 6660, 58244, 58978, 23918, 88206, 113611, 64452, 17541, 41032, 10942, 12021, + 49189, 10978, 40175, 37156, 10947, 71709, 106894, 112538, 57007, 137486, 150608, 152719, 40615, 7746, 279716, 13101, + 19524, 28708, 40578, 72320, 1096, 182051, 94527, 51275, 22833, 45164, 81917, 77519, 48508, 5421, 140302, 37845, + 149830, 5587, 27579, 5357, 428725, 248187, 6326, 206760, 39814, 32585, 89923, 44341, 288753, 284443, 96368, 31201, + 94189, 119504, 20359, 52073, 103216, 179, 27934, 32801, 96035, 34111, 34309, 101326, 18198, 20704, 210266, 37643, + 27880, 141873, 106000, 19414, 56614, 167714, 66483, 107885, 86602, 4379, 20796, 75467, 4987, 5017, 118857, 26003, + 34308, 114428, 29198, 6686, 29697, 73632, 3739, 69795, 16798, 41504, 7207, 30722, 21436, 36735, 28067, 28545, + 3239, 11221, 36031, 41889, 100010, 19247, 317673, 29495, 174554, 6424, 129725, 53845, 94986, 7955, 59676, 2604, + 191497, 19735, 102214, 62954, 23844, 11872, 179525, 261436, 34492, 428, 78404, 142035, 16747, 17246, 27578, 37021, + 33672, 57944, 26056, 135760, 2369, 61674, 122066, 31327, 19374, 157065, 40553, 130982, 69619, 71290, 38855, 72100, + 92903, 95940, 51422, 165999, 65713, 57873, 50726, 7288, 20272, 2081, 42326, 22624, 81120, 57914, 79352, 19447, + 1684, 72302, 11774, 302559, 161481, 96396, 13692, 414988, 3721, 79066, 56627, 46883, 21150, 11747, 12184, 5856, + 113458, 176117, 84416, 52079, 27933, 3354, 59765, 141359, 2212, 216309, 2555, 23458, 196722, 142463, 45701, 44548, + 28798, 19418, 215, 29916, 9396, 10574, 114226, 84475, 13520, 18694, 34056, 4524, 90302, 62930, 13539, 19407, + 77209, 7728, 38088, 9535, 2263, 23875, 183945, 17750, 26274, 67172, 10585, 28042, 22199, 7478, 51331, 66030, + 26774, 192929, 31434, 25850, 50197, 52926, 178158, 4679, 181256, 70184, 229600, 9959, 105594, 72158, 73974, 2726, + 35085, 78087, 23284, 35568, 51713, 155676, 5401, 27254, 11966, 17569, 223253, 71993, 103357, 111477, 55722, 30504, + 26034, 46774, 35392, 36285, 214814, 41143, 163465, 1051, 16094, 81044, 6636, 76489, 179102, 20712, 39178, 35683, + 125177, 54219, 30617, 52994, 25324, 50123, 2543, 87529, 58995, 10688, 125199, 12388, 60158, 125481, 131646, 7642, + 133350, 65874, 3438, 97277, 101450, 10075, 56344, 116821, 50778, 60547, 98016, 106135, 13859, 14255, 16300, 77373, + 173521, 8285, 45932, 37426, 4054, 114295, 55947, 7703, 39114, 52, 51119, 128135, 19714, 60715, 9554, 50492, + 88180, 2823, 118271, 52993, 122625, 97919, 23859, 37895, 25040, 33614, 32102, 20431, 3577, 9275, 15686, 43031, + 157741, 110358, 1884, 40291, 125391, 13736, 5008, 64881, 87336, 77381, 70711, 43032, 49155, 118587, 70494, 4318, + 10168, 30126, 12580, 10524, 280104, 104001, 145413, 2862, 84140, 6603, 106005, 13566, 12780, 11251, 42830, 571, + 179910, 82443, 13146, 469, 42714, 32591, 265217, 424024, 92553, 54721, 134100, 6007, 15242, 114681, 59030, 16718, + 85465, 200214, 85982, 55174, 165013, 23493, 56964, 82529, 109150, 32706, 27568, 82442, 5350, 14976, 13165, 44890, + 60021, 21343, 33978, 17264, 4655, 22328, 27819, 75730, 16567, 55483, 14510, 17926, 45827, 150609, 3704, 7385, + 272531, 161543, 76904, 122163, 52405, 2039, 19165, 41623, 14423, 228354, 3369, 176360, 85491, 7122, 35789, 303724, + 4465, 13628, 2233, 55311, 118771, 20713, 10006, 221519, 45115, 71021, 35650, 29775, 7337, 10864, 20665, 21142, + 1746, 15080, 1624, 32449, 10905, 105743, 229797, 7701, 3940, 22997, 178467, 57208, 389057, 39683, 59403, 63344, + 63125, 54847, 69691, 18336, 56448, 3362, 37202, 18282, 29648, 138224, 35867, 10495, 5911, 28814, 26653, 31514, + 176702, 26550, 45621, 11734, 4525, 40543, 73944, 121080, 27858, 155561, 14887, 44670, 30742, 8796, 107455, 113472, + 56369, 75581, 183777, 240095, 133699, 153299, 8768, 160464, 26058, 49078, 103971, 21875, 71486, 44888, 17156, 9678, + 89541, 123019, 102337, 3972, 83930, 21245, 87852, 109660, 287918, 183019, 686, 10100, 39177, 283941, 11274, 24736, + 26793, 26214, 25995, 77011, 141580, 4070, 23742, 46285, 46632, 30700, 26669, 19056, 35951, 115575, 174034, 56097, + 35463, 87425, 24575, 44245, 38701, 82317, 85922, 281616, 100333, 147697, 61503, 7730, 84330, 8530, 59917, 61597, + 17173, 9092, 32658, 90288, 193136, 39023, 20381, 56654, 31132, 7779, 1919, 1375, 117128, 30819, 11169, 40938, + 23935, 115201, 101155, 151034, 4835, 11231, 74550, 89388, 59951, 91704, 107312, 167882, 115062, 12732, 72738, 88703, + 464019, 158267, 57995, 60496, 737, 14371, 123867, 4174, 243339, 159946, 7568, 16025, 134556, 110916, 38103, 191, + 80226, 88794, 29688, 27230, 10454, 76308, 57647, 77409, 113483, 66864, 14745, 19808, 12023, 46583, 84805, 16015, + 17102, 2231, 20611, 3547, 95740, 250131, 34559, 108894, 8498, 15853, 159169, 148920, 20942, 2813, 93160, 45188, + 210613, 45531, 52587, 149062, 39782, 28194, 57849, 60965, 84954, 89766, 84453, 100927, 16501, 27658, 165311, 103841, + 54192, 207341, 19558, 20084, 319622, 5672, 205467, 98462, 61849, 36279, 13609, 147177, 24726, 165015, 209489, 59591, + 31157, 6551, 117580, 75060, 141146, 277310, 21072, 22023, 106474, 63041, 137443, 122965, 68371, 5383, 42146, 98961, + 113467, 30863, 23794, 4843, 99630, 30392, 82679, 13699, 241612, 33601, 93146, 24319, 18643, 32155, 95669, 40440, + 15333, 34089, 67799, 142144, 58245, 38633, 114531, 117400, 77861, 188726, 5507, 2568, 8853, 10987, 107222, 2663, + 2421, 11530, 13345, 30075, 41785, 118661, 104786, 17459, 12490, 16281, 71936, 193555, 17431, 5944, 71758, 26485, + 77317, 20803, 367167, 158, 7362, 93430, 11735, 172445, 46002, 11532, 54482, 930, 62911, 2235, 23004, 179236, + 4764, 101859, 208113, 22477, 55163, 95579, 14098, 67320, 162556, 90709, 156949, 3826, 57492, 4025, 34092, 87442, + 104565, 6718, 186015, 28214, 14209, 10039, 107186, 233912, 58877, 81637, 55265, 39828, 6194, 145813, 50831, 105849, + 4974, 88319, 122296, 10272, 197216, 95714, 51540, 72418, 23324, 91555, 8743, 140452, 250249, 51666, 34124, 7229, + 38592, 129641, 78169, 174242, 22464, 149964, 51450, 14034, 10026, 95376, 26190, 120062, 14401, 8700, 265, 31386, + 143573, 7203, 229889, 61567, 4227, 140981, 2466, 72052, 10787, 10062, 30958, 6099, 38471, 30103, 23202, 208101, + 70847, 467, 58934, 32271, 32984, 36637, 24107, 30771, 17109, 73353, 13650, 2098, 157040, 67366, 66904, 106018, + 265380, 107238, 18535, 44025, 32681, 144983, 62505, 91295, 56120, 3082, 77508, 10322, 63023, 36700, 81885, 224127, + 16721, 45023, 239261, 111272, 13852, 7866, 149243, 204199, 32309, 22084, 42029, 38316, 126644, 104973, 14406, 43454, + 67322, 61310, 15789, 40285, 24026, 181047, 6301, 70927, 23319, 115823, 27248, 66693, 115875, 278566, 63007, 146844, + 56841, 59007, 87368, 180001, 22370, 42114, 80605, 12022, 10374, 308, 25079, 14689, 12618, 63368, 7936, 264973, + 212291, 136713, 95999, 105801, 18965, 32075, 48700, 52230, 35119, 96912, 32992, 8586, 16606, 101333, 101812, 14969, + 39930, 759, 193090, 27387, 42914, 12937, 5058, 62646, 64528, 38624, 25743, 37502, 3716, 4435, 30352, 178687, + 26461, 132611, 42002, 138442, 35833, 59582, 16345, 8048, 60319, 49349, 309, 47800, 49739, 90482, 26405, 34470, + 63786, 32479, 85028, 39866, 47846, 11649, 23934, 29466, 2816, 42864, 31828, 7410, 74885, 49632, 47629, 111801, + 90749, 19536, 18767, 105764, 59606, 21223, 10746, 76298, 22220, 39408, 7190, 79654, 64856, 11602, 82156, 272765, + 17079, 70089, 245473, 51813, 184407, 384678, 1576, 122249, 5064, 27481, 6188, 25790, 74361, 27541, 318284, 45430, + 31488, 620, 93579, 45723, 192118, 22670, 51913, 4162, 70244, 35966, 26397, 16199, 50899, 209613, 121702, 287507, + 2993, 36101, 132229, 67345, 33062, 76295, 118628, 78705, 52316, 34375, 107083, 107454, 44863, 127561, 33964, 3073, + 154010, 190914, 55967, 39074, 6272, 31047, 5550, 41123, 26154, 98638, 47110, 19998, 148091, 50229, 31329, 59900, + 195442, 19106, 61347, 73497, 70015, 682, 45850, 25776, 38022, 148951, 6288, 37411, 232526, 109277, 27286, 32342, + 9262, 5220, 16651, 23175, 46740, 129438, 78614, 121925, 66914, 88710, 127952, 5563, 21500, 34521, 10739, 14863, + 191006, 62956, 17359, 16749, 67027, 56284, 69134, 43301, 35039, 58883, 54466, 60823, 404451, 75743, 59856, 86979, + 7923, 34273, 83785, 32142, 7693, 268986, 197428, 282681, 17049, 22346, 22990, 92245, 107180, 3357, 37104, 96724, + 49153, 7683, 31197, 43267, 82231, 164276, 23696, 20848, 188364, 22309, 24821, 158707, 1018, 22514, 70922, 27792, + 45589, 59709, 10765, 736, 35218, 63479, 51987, 24275, 63588, 55361, 92929, 81964, 4658, 20122, 12330, 44058, + 13065, 311456, 72224, 8337, 211229, 38979, 22590, 138478, 52757, 32595, 133600, 8838, 31549, 94412, 43391, 90056, + 1585, 94802, 127271, 6223, 31889, 137038, 132910, 2165, 57616, 230152, 6080, 10748, 36737, 74579, 134062, 50525, + 180532, 119270, 34556, 76155, 82394, 52595, 29258, 31435, 87820, 67996, 26943, 183878, 38007, 2410, 13526, 180297, + 69856, 3503, 187396, 167700, 7838, 16701, 9199, 56267, 3661, 37407, 65994, 23767, 5708, 62508, 221700, 67088, + 86978, 46776, 84434, 32088, 5612, 9149, 88244, 21685, 95151, 46750, 189612, 2979, 506311, 2594, 3628, 40074, + 105039, 78243, 28523, 6651, 38058, 71999, 30992, 12764, 68261, 108991, 6165, 26450, 61961, 13400, 22426, 7490, + 60890, 109623, 2070, 12958, 50355, 67979, 257096, 7213, 42578, 52121, 35716, 65461, 7516, 124758, 39268, 302, + 64712, 14977, 1467, 219452, 2840, 34229, 11121, 21602, 19270, 63574, 8024, 1532, 17331, 79839, 78885, 52029, + 180767, 57957, 6069, 91265, 61380, 55767, 8927, 32881, 287603, 22149, 35029, 68876, 6428, 199567, 46926, 13412, + 104132, 21434, 366616, 45060, 110046, 81924, 128910, 45886, 52821, 130416, 29416, 77342, 21762, 67329, 121432, 79924, + 11724, 38625, 81006, 102033, 28338, 13326, 3250, 82056, 82526, 38212, 21112, 12382, 111495, 3263, 7414, 86274, + 93490, 40844, 30224, 45212, 24019, 48411, 71367, 24941, 76729, 57776, 3769, 38114, 202019, 197745, 31953, 237533, + 33270, 201580, 255648, 100798, 44741, 32241, 98468, 106931, 10085, 15090, 170358, 33154, 66787, 18819, 69760, 25061, + 234005, 82660, 6295, 131975, 16874, 9076, 4094, 25005, 17740, 40908, 19533, 220019, 44330, 99792, 50040, 19619, + 13950, 55228, 24423, 31253, 95308, 103177, 184795, 28590, 82285, 5059, 3210, 75525, 49894, 70007, 56178, 10580, + 36051, 139681, 21617, 98736, 3555, 106306, 164189, 37352, 63915, 47824, 24883, 145530, 61904, 28444, 11483, 19837, + 145446, 30420, 112972, 85939, 11835, 191233, 2262, 20705, 58630, 1753, 148334, 1197, 144714, 6887, 11223, 107667, + 60879, 77914, 4151, 57417, 81594, 96681, 169430, 1784, 20444, 95138, 254041, 27038, 596, 7117, 72808, 13759, + 3353, 126776, 21074, 55322, 27081, 36942, 39547, 139830, 179275, 4453, 713, 8722, 71399, 19204, 25785, 22794, + 23923, 104114, 11291, 25458, 102309, 88396, 75288, 230440, 206396, 104551, 58447, 130857, 37247, 94734, 31548, 176529, + 226077, 65159, 20104, 10096, 66881, 94191, 237909, 27109, 37404, 1520, 27421, 25220, 113003, 23423, 24884, 50585, + 6286, 231877, 150800, 11789, 3226, 90004, 60642, 5053, 202400, 61442, 132531, 175329, 57138, 30116, 103847, 9973, + 75367, 16452, 32360, 59119, 21246, 10191, 164804, 23305, 61051, 37348, 154530, 13214, 5468, 50403, 66754, 130976, + 50559, 80515, 14436, 155492, 84017, 5472, 43107, 41240, 2890, 90431, 70188, 382, 76234, 48040, 50211, 281038, + 237007, 32115, 142178, 1536, 22761, 96429, 1811, 31243, 1679, 49143, 55209, 17402, 235054, 61494, 7462, 77030, + 34925, 87609, 78002, 9499, 9027, 73289, 201078, 101379, 63544, 27666, 5469, 10642, 30029, 49816, 132979, 95620, + 58086, 351930, 116300, 2110, 2043, 30845, 6154, 11279, 16727, 4122, 2277, 27281, 4971, 3650, 39060, 61970, + 65951, 39674, 75686, 38151, 11370, 130809, 177895, 32665, 63725, 122267, 7857, 39618, 118483, 44792, 157755, 178624, + 136994, 24260, 41308, 22471, 12404, 21707, 12486, 30473, 52781, 50246, 20247, 39065, 909, 56825, 103158, 128603, + 31542, 1089, 41935, 32744, 12428, 37963, 84420, 33134, 72921, 208449, 42622, 168151, 127335, 147107, 46699, 38216, + 12591, 94342, 85814, 31423, 24944, 2605, 87542, 67473, 192551, 4496, 56321, 91819, 17630, 6300, 256183, 114569, + 202090, 33209, 35289, 34897, 24967, 40520, 43470, 5344, 10199, 34810, 14283, 10381, 10017, 62923, 49924, 23233, + 64539, 13051, 35686, 19698, 11570, 135555, 120868, 44924, 87065, 52318, 52335, 47586, 140906, 245885, 109834, 78668, + 9065, 46990, 25258, 72022, 61243, 40838, 4545, 146387, 10537, 11557, 17470, 36930, 68104, 46711, 24264, 79401, + 81043, 18225, 120488, 24746, 84338, 81652, 28266, 13776, 21878, 46973, 1047, 230465, 73357, 95777, 24973, 210160, + 62210, 58404, 110633, 169651, 6937, 41870, 9909, 26822, 191062, 76553, 27519, 96256, 239070, 2478, 205678, 67955, + 58532, 20601, 50120, 19148, 78501, 195724, 110740, 8249, 109665, 27446, 30568, 57631, 31425, 49752, 32820, 65504, + 50079, 3663, 102256, 219898, 23849, 211315, 14645, 4359, 91767, 9528, 12449, 49366, 7941, 49763, 107848, 8930, + 27086, 50686, 9744, 10447, 81935, 39513, 46514, 1670, 29229, 6172, 22312, 137280, 97759, 9806, 14445, 22976, + 56458, 73391, 34983, 93760, 174219, 52573, 33149, 59747, 2429, 136277, 75123, 165263, 91040, 7446, 57632, 48633, + 97140, 246081, 84766, 151684, 79918, 93268, 120346, 54059, 54875, 77858, 32996, 103590, 45276, 11968, 19600, 25849, + 17159, 132907, 42828, 16817, 4913, 99462, 103303, 27395, 5737, 74184, 20749, 21160, 14377, 77062, 131403, 158735, + 10999, 27799, 77785, 9320, 34366, 51593, 61070, 33746, 47048, 29268, 36675, 30262, 53297, 9832, 82000, 20188, + 122292, 39917, 7331, 18160, 68301, 185935, 134830, 15031, 4935, 10004, 165845, 185534, 46923, 30109, 44134, 122631, + 18874, 22903, 112790, 26561, 18549, 348902, 82871, 140345, 255565, 135390, 63556, 103747, 145055, 179600, 145662, 296111, + 61661, 211987, 23952, 52342, 126343, 48450, 32919, 44277, 82185, 9591, 62139, 205363, 376969, 394874, 108461, 18040, + 120885, 14798, 39863, 16571, 16794, 58271, 81025, 55206, 14640, 118656, 6361, 44092, 85970, 6262, 153863, 108244, + 180200, 72264, 79947, 38044, 10050, 5735, 61221, 80712, 5471, 115689, 11391, 11661, 184257, 20010, 60116, 30320, + 19327, 134598, 45455, 27542, 18004, 125092, 452272, 1549, 91523, 46567, 180063, 156026, 2608, 11174, 58848, 37788, + 65907, 80194, 30490, 5786, 40775, 119519, 106241, 11323, 156297, 8425, 61495, 2617, 29675, 2425, 59886, 112582, + 49142, 59618, 4863, 50597, 86710, 50650, 168632, 27693, 85641, 83643, 18993, 25768, 84284, 28090, 93592, 36627, + 312804, 43381, 9887, 9402, 100931, 97165, 3311, 173330, 66805, 28935, 4963, 184460, 3201, 78102, 19126, 21607, + 37496, 24938, 22615, 16153, 32862, 134792, 153318, 61120, 6067, 2812, 12826, 12792, 23825, 37559, 64662, 202250, + 102694, 155488, 85881, 149193, 46233, 65383, 15521, 106982, 11358, 176786, 25752, 39717, 34208, 24510, 32464, 77742, + 39371, 72028, 138229, 60688, 71386, 102834, 132477, 2208, 11548, 63670, 271279, 28351, 30338, 38620, 32491, 99845, + 143885, 152266, 13252, 2825, 178663, 108097, 1775, 78201, 14897, 113573, 163346, 62292, 171129, 22183, 96598, 38733, + 64971, 166776, 117445, 9968, 146393, 44677, 74867, 20908, 97328, 12761, 25656, 26785, 9148, 112344, 26115, 99176, + 110121, 22437, 49547, 6180, 79320, 5835, 31392, 43328, 33377, 75870, 119860, 69497, 80273, 7325, 155219, 43167, + 111173, 28347, 20222, 3763, 71752, 55041, 47252, 14618, 28088, 15012, 97805, 194698, 54636, 2036, 41349, 6173, + 96604, 61530, 51859, 43782, 13361, 24334, 22668, 24792, 7070, 23441, 16789, 3209, 36211, 208475, 26242, 32880, + 122181, 182407, 21444, 31060, 88459, 29929, 77907, 12716, 10934, 97005, 20599, 31690, 8403, 58445, 30303, 22700, + 10336, 86731, 103115, 337709, 72556, 46788, 112566, 47684, 67089, 53548, 36874, 56487, 41387, 125985, 26893, 40071, + 106683, 73712, 18787, 40105, 72992, 67246, 137276, 50802, 36790, 70328, 138827, 22466, 39263, 183295, 29858, 50975, + 9322, 57397, 10654, 24364, 30383, 55799, 41600, 23584, 127295, 296610, 129078, 143558, 244131, 86397, 36049, 1085, + 80677, 3820, 108139, 5476, 34767, 24683, 7758, 13060, 7239, 131671, 250593, 59556, 103392, 29810, 4188, 252323, + 39404, 116877, 7651, 43600, 40338, 13554, 157253, 39196, 25978, 144387, 61211, 234, 50104, 6129, 10449, 93777, + 9240, 356378, 274148, 4439, 72970, 3724, 147770, 78680, 62570, 115877, 40027, 40547, 36817, 224392, 64609, 34795, + 165027, 67440, 2477, 37206, 23431, 50754, 164797, 46018, 94995, 170982, 27051, 7957, 22767, 3674, 27900, 56419, + 18930, 60701, 41302, 2692, 84749, 339721, 61996, 111094, 80221, 50129, 1045, 8153, 62945, 19202, 8250, 37208, + 37418, 32560, 79477, 41106, 88569, 33963, 36693, 5892, 30570, 1581, 66471, 49647, 11922, 160717, 29442, 5643, + 114865, 82962, 95982, 132098, 22633, 22838, 94726, 54556, 28566, 205039, 162340, 33216, 16849, 35847, 221339, 94851, + 26533, 71469, 1805, 3804, 12935, 45483, 71020, 36310, 65381, 192960, 34240, 35165, 59773, 1248, 46954, 155332, + 96864, 4246, 388800, 16129, 57133, 74592, 44807, 442014, 38203, 42574, 80818, 91592, 26377, 36424, 65760, 977, + 77387, 22628, 147610, 28018, 30561, 98454, 6969, 119628, 63648, 18170, 36854, 26601, 64018, 22027, 37279, 51395, + 152934, 21153, 9430, 58760, 194742, 5330, 55115, 34158, 28917, 174111, 13171, 122326, 1526, 43896, 66094, 25325, + 4234, 148354, 11450, 275, 18999, 112191, 44365, 22723, 68409, 8733, 57746, 96565, 75007, 14196, 108844, 29475, + 88599, 177563, 100792, 106156, 86323, 93726, 14248, 135341, 194131, 40126, 47099, 14779, 8272, 39597, 95983, 171398, + 65882, 28052, 10393, 47213, 40689, 22120, 72212, 106829, 34964, 109146, 753, 648, 21660, 30047, 17527, 181025, + 5619, 145357, 4085, 216883, 9359, 186951, 24779, 53931, 24545, 36197, 223296, 62628, 168101, 4243, 107313, 30321, + 26642, 13049, 51059, 31027, 107912, 807, 73550, 26551, 84369, 122422, 165872, 49754, 74213, 234264, 33151, 52014, + 33100, 87183, 22365, 52500, 40013, 23302, 5652, 72723, 21404, 26107, 48434, 587, 94049, 168493, 96418, 32871, + 70860, 31709, 25128, 443, 71597, 166253, 15670, 70994, 26341, 133675, 28280, 75491, 54756, 47955, 56028, 26182, + 11952, 113272, 472197, 64640, 110753, 17919, 337, 50642, 22576, 142, 87371, 53391, 93210, 126694, 15285, 19642, + 85667, 14148, 1506, 42092, 52962, 33243, 11970, 20734, 135843, 57044, 58880, 13002, 219134, 22876, 64754, 232519, + 4257, 43120, 321573, 24799, 64526, 124728, 52579, 81472, 70831, 276848, 17403, 74359, 23021, 182101, 74597, 23744, + 148267, 12055, 7976, 5349, 11772, 67540, 167347, 65318, 18720, 127832, 108238, 22828, 90233, 9987, 259080, 118185, + 73209, 79270, 13775, 90100, 137742, 90799, 70569, 15699, 19961, 9087, 67475, 57872, 39731, 8810, 134897, 131868, + 146849, 19898, 3334, 2281, 167061, 91073, 60356, 467742, 74712, 188, 53179, 137679, 92769, 29241, 9537, 132595, + 80119, 1041, 88962, 5976, 40171, 44911, 102859, 139059, 104558, 98987, 47761, 19272, 71472, 113864, 175377, 73338, + 10857, 23402, 23758, 1591, 139864, 5644, 4076, 118760, 16427, 134198, 18853, 20291, 100849, 37423, 22038, 36677, + 19071, 195521, 57445, 11069, 31869, 55718, 66882, 148490, 44, 41296, 75242, 49704, 166810, 9906, 20943, 122258, + 49112, 105667, 15969, 10344, 6408, 187694, 21399, 72742, 58970, 14867, 14376, 81889, 41856, 23225, 15042, 56993, + 16074, 131389, 74276, 72407, 53875, 383108, 53597, 37363, 68993, 44854, 122548, 430927, 198279, 38430, 80409, 12245, + 2981, 628, 2818, 17760, 37437, 238229, 7968, 46892, 2200, 3730, 34190, 65983, 37959, 112291, 87850, 70827, + 6522, 20750, 73913, 111621, 41652, 19587, 2780, 58668, 25916, 85259, 18200, 168962, 95781, 42445, 102050, 7776, + 57662, 103313, 47742, 96358, 41964, 66174, 100396, 29069, 204735, 19679, 27978, 7479, 40264, 22534, 61183, 36081, + 107436, 58223, 14680, 23002, 101311, 24716, 124108, 12908, 5646, 31750, 40380, 14215, 232799, 102772, 14122, 96775, + 61398, 50917, 12096, 149880, 67833, 598749, 124194, 155871, 49216, 790, 14677, 65319, 56917, 7440, 145744, 95701, + 12206, 49405, 129269, 76199, 45732, 9767, 11058, 9047, 210885, 11051, 7392, 26307, 2130, 8132, 147526, 20802, + 232698, 115660, 50060, 59789, 57344, 107623, 80343, 112676, 23291, 9866, 160971, 34032, 118291, 15719, 59730, 164911, + 28975, 2659, 58046, 78480, 21854, 66209, 53863, 109085, 116045, 29021, 46481, 107552, 22130, 18764, 70254, 31272, + 11300, 52460, 43933, 84738, 20721, 53869, 190840, 79673, 105300, 7561, 321817, 66924, 13940, 33281, 101046, 183181, + 32176, 71878, 5678, 62924, 79535, 56646, 40303, 19559, 27703, 93042, 73368, 42187, 3670, 37376, 46440, 7023, + 36816, 109628, 20680, 5940, 276440, 275233, 170848, 112093, 136996, 14984, 20226, 111441, 77693, 112960, 48577, 39370, + 55707, 50314, 123404, 26570, 54281, 61372, 123391, 4857, 35928, 246740, 132507, 106646, 44241, 7196, 92258, 9825, + 37688, 51197, 303141, 5590, 15476, 132986, 10955, 85782, 34486, 26696, 7991, 28813, 18858, 39546, 11703, 11365, + 38185, 5716, 93555, 11925, 40121, 60002, 6985, 10976, 171384, 3887, 43394, 13337, 56346, 6381, 252336, 39573, + 75042, 53711, 1028, 31781, 44295, 95925, 131713, 7214, 68125, 43571, 70954, 213234, 1628, 8760, 13391, 65485, + 17320, 56038, 1710, 25248, 60803, 57399, 19839, 3870, 326, 281556, 50945, 72400, 21460, 316244, 75619, 56246, + 98775, 481, 13513, 55765, 50427, 7388, 123519, 32929, 57908, 27124, 61316, 101097, 57467, 30228, 48792, 10788, + 20402, 37318, 50526, 155730, 34456, 158065, 145305, 17832, 43733, 64052, 4506, 35072, 205355, 177028, 184004, 187081, + 68616, 35938, 83703, 10367, 36892, 93186, 260137, 51934, 89970, 4985, 23445, 26755, 21558, 7948, 78741, 23376, + 124405, 85594, 68596, 57536, 49351, 12619, 56593, 132668, 99924, 109728, 71844, 71935, 196018, 65464, 17617, 14987, + 89701, 143773, 33997, 8687, 22701, 33258, 2914, 4436, 72108, 85610, 9671, 49067, 2327, 82988, 1361, 1672, + 44033, 35777, 30269, 24057, 10605, 82236, 616, 15793, 13919, 47249, 112086, 116698, 9484, 80207, 90574, 33304, + 68624, 93127, 56101, 42210, 160929, 4827, 38995, 38095, 4701, 125119, 5027, 33680, 9236, 231236, 14135, 87837, + 23318, 70261, 78893, 30151, 81482, 14332, 1084, 74256, 27532, 46644, 79185, 3148, 62615, 6981, 55672, 31668, + 36825, 1849, 14536, 37446, 14738, 23779, 43058, 162749, 72199, 1168, 21346, 5592, 85932, 85302, 9668, 18351, + 57135, 150360, 2080, 228015, 77953, 34670, 119302, 151751, 31009, 106725, 84265, 45214, 59289, 74178, 113071, 263206, + 111009, 4021, 44449, 188119, 192629, 123592, 392506, 292847, 114487, 12831, 205858, 9852, 20780, 79648, 75767, 357014, + 97721, 18166, 21005, 67950, 33226, 204009, 16536, 2987, 11335, 66717, 144910, 47950, 17262, 55060, 15063, 2934, + 51038, 26775, 178497, 66008, 3427, 49433, 128592, 20036, 157553, 63861, 3089, 23015, 51210, 28696, 35933, 49942, + 71135, 231518, 99620, 17248, 21835, 176536, 20676, 16944, 38700, 165831, 233253, 295625, 36723, 13023, 52745, 10907, + 19423, 67972, 125868, 95473, 82875, 1183, 108455, 52685, 33417, 64095, 21433, 52438, 33191, 127809, 44505, 211823, + 7810, 2752, 95548, 162031, 7185, 91196, 47563, 61721, 33359, 17897, 23682, 42806, 178101, 22874, 49707, 199897, + 75419, 82456, 8618, 11171, 79712, 116847, 18783, 44190, 46564, 5346, 59046, 95032, 7893, 14916, 3214, 26800, + 24172, 121453, 34362, 10250, 17408, 18888, 4840, 68696, 22831, 13162, 36005, 32512, 14800, 62357, 41723, 45046, + 27247, 37486, 5372, 2564, 34261, 298500, 66509, 133920, 89138, 31305, 117697, 19097, 108304, 81386, 84106, 23802, + 46411, 63304, 946, 51417, 41777, 41041, 19501, 115864, 60743, 294354, 37955, 94165, 18116, 1156, 17937, 20645, + 57114, 90804, 58042, 48643, 92288, 9861, 2557, 88546, 61333, 101008, 12853, 5148, 87856, 4152, 144503, 73841, + 18718, 9789, 147565, 10846, 42085, 12789, 30223, 8993, 56352, 67203, 2448, 28215, 6052, 23540, 126319, 75933, + 36689, 80235, 23231, 23561, 21383, 38800, 77548, 102798, 21234, 31468, 158608, 46188, 63960, 191679, 8051, 67014, + 11185, 170078, 42186, 28827, 34777, 41930, 212079, 12421, 34750, 24111, 110344, 73918, 45171, 70826, 141949, 40063, + 23979, 24254, 37309, 26724, 27179, 24718, 83648, 54938, 14591, 17425, 29525, 102675, 48975, 48654, 12316, 8929, + 60640, 41709, 50168, 63264, 89812, 50716, 48632, 38755, 138583, 160123, 55579, 71829, 24230, 233277, 46322, 39650, + 166388, 34718, 24108, 98252, 7031, 106695, 62498, 18258, 35062, 217827, 78731, 34824, 33354, 19520, 60852, 2432, + 60224, 8587, 2836, 62955, 702, 20227, 42285, 40560, 95592, 62486, 11094, 53035, 143291, 18842, 46177, 77994, + 1770, 9657, 107422, 172915, 32655, 128716, 25886, 25164, 156740, 119928, 165875, 85817, 11007, 89110, 33956, 12652, + 65156, 180266, 8494, 36889, 19958, 20955, 96, 1264, 118288, 135769, 44754, 86671, 5632, 19026, 168220, 289120, + 33569, 93821, 66144, 70635, 7687, 5642, 2714, 55445, 56636, 71545, 184182, 93133, 7332, 37389, 12643, 52315, + 22729, 11014, 158742, 17050, 152889, 50178, 34601, 41945, 52136, 9948, 26914, 63548, 95721, 115951, 40759, 8960, + 158258, 38938, 49232, 48325, 42234, 81523, 253019, 66128, 40978, 20048, 238048, 38760, 62928, 122560, 118532, 43687, + 137472, 163689, 26680, 9878, 17448, 51035, 16211, 60834, 36749, 29178, 14241, 59868, 150086, 2305, 26477, 42422, + 34342, 165341, 83279, 33894, 14257, 29928, 12743, 13957, 125571, 89134, 66712, 10952, 16507, 147839, 30146, 7249, + 16565, 45399, 39874, 114565, 215780, 31990, 230881, 171477, 102, 196546, 44538, 10880, 84948, 281705, 86651, 10617, + 31395, 2342, 453658, 43569, 60561, 132901, 21845, 17727, 58556, 258242, 22262, 58728, 4008, 77997, 11806, 37431, + 30599, 81375, 109137, 185787, 114085, 217292, 97453, 169085, 30593, 60212, 11544, 102056, 65580, 2384, 91655, 4855, + 95725, 7295, 157994, 16228, 20669, 53276, 141590, 105246, 17334, 25440, 76067, 17967, 39321, 38911, 11362, 28559, + 63807, 21627, 26468, 85816, 40120, 1025, 15234, 58319, 69516, 66512, 124548, 75845, 78873, 22137, 46681, 51242, + 85683, 32909, 76747, 35555, 43396, 101465, 1765, 73094, 1077, 2962, 39028, 66777, 57831, 42048, 15828, 13962, + 36041, 63657, 52412, 5242, 58846, 2141, 5506, 219012, 134451, 3936, 182230, 17558, 17153, 152237, 22621, 49377, + 170216, 35257, 68233, 65374, 6510, 11126, 212151, 7184, 2480, 22517, 3437, 33073, 30156, 16557, 3768, 55067, + 86829, 91000, 12350, 148650, 66017, 79424, 70885, 49066, 28250, 21369, 51213, 34533, 11510, 3258, 18176, 18465, + 84413, 6315, 36411, 163765, 4346, 356, 107618, 598, 13727, 285026, 162695, 8749, 14583, 7132, 63521, 184253, + 32378, 25991, 5604, 30961, 53675, 4874, 84693, 5086, 34811, 26978, 56564, 7904, 33519, 51221, 113942, 69253, + 6664, 125563, 22055, 220680, 102008, 742, 51930, 19494, 176108, 44424, 35123, 13025, 75685, 11759, 74335, 22250, + 181453, 131147, 16984, 132115, 154311, 11991, 76452, 52609, 85351, 196, 30969, 9198, 74919, 2529, 56838, 71779, + 29187, 116304, 3504, 62330, 41190, 86153, 28393, 254926, 104228, 105189, 13264, 84359, 3574, 12415, 8534, 57147, + 10175, 188174, 59504, 60932, 66318, 16407, 107921, 17638, 99103, 49278, 28403, 39786, 145865, 8462, 3558, 43406, + 142271, 29139, 21989, 36552, 93955, 72365, 7176, 13556, 106185, 37957, 321774, 17782, 129017, 51154, 27938, 24952, + 1935, 39366, 2791, 33489, 41582, 56078, 24558, 9311, 5449, 218786, 27808, 190429, 68013, 36020, 86003, 29735, + 3404, 87348, 119357, 115714, 2324, 86796, 81973, 40992, 43376, 93621, 28784, 16808, 36367, 2517, 2909, 191926, + 24978, 55303, 53308, 205724, 60068, 3098, 21375, 64784, 23949, 26579, 63121, 12319, 80145, 39967, 97861, 6757, + 70143, 67642, 37082, 34698, 69140, 122883, 46151, 62187, 80934, 429, 19437, 135071, 137885, 222647, 13331, 154065, + 327, 61778, 74257, 40116, 37493, 14855, 85079, 237641, 42342, 102164, 199965, 71204, 4662, 29368, 5042, 113914, + 122214, 8955, 13149, 102503, 43173, 5659, 163787, 69003, 307084, 63392, 171080, 21390, 81918, 86666, 36622, 24126, + 28887, 5736, 28054, 207170, 163428, 79891, 346467, 95363, 38980, 111806, 80828, 9200, 19288, 294896, 114468, 87405, + 111715, 141705, 7015, 72754, 68463, 48738, 243147, 33397, 101210, 37051, 98801, 82847, 20397, 4940, 185559, 18716, + 54718, 83491, 11725, 40803, 1128, 12128, 23060, 5174, 7745, 67007, 46701, 1571, 27807, 180186, 256996, 18975, + 16837, 7877, 212758, 250379, 15440, 87954, 57755, 24719, 124057, 83461, 258, 50864, 8874, 29038, 71289, 31627, + 15429, 9005, 4061, 113851, 107716, 82819, 13651, 79656, 117851, 17539, 111446, 12938, 39724, 190787, 4352, 15402, + 21070, 62708, 8539, 23777, 73853, 13552, 38810, 86117, 16285, 56400, 1718, 75342, 142863, 29033, 378, 110113, + 180321, 32586, 23606, 26393, 160984, 207987, 23783, 8406, 16904, 24596, 47274, 11693, 46539, 60524, 78595, 48423, + 31718, 20170, 9009, 146268, 15183, 191060, 172765, 1349, 138436, 37365, 10970, 40509, 225817, 20021, 70394, 152138, + 21541, 66559, 66544, 89352, 2725, 17258, 91345, 7313, 3815, 115868, 8660, 40362, 4071, 103524, 39388, 118275, + 21950, 6549, 38226, 32754, 209574, 29201, 43495, 18028, 20296, 40597, 18370, 47520, 202450, 24134, 2219, 8195, + 69545, 38041, 136934, 46374, 19041, 159811, 84865, 58620, 846, 98749, 13569, 30714, 97246, 32186, 4479, 27355, + 92973, 35214, 151491, 75963, 37631, 1561, 27200, 238083, 23182, 60756, 12291, 25766, 39355, 102333, 87362, 65741, + 59906, 19538, 201575, 48772, 102938, 24438, 292580, 39964, 66366, 9004, 61379, 50548, 37622, 38732, 28379, 68180, + 76622, 17488, 69849, 5963, 7219, 48143, 43413, 55358, 540, 58691, 29506, 19245, 52193, 48621, 5518, 13048, + 118625, 44755, 191081, 42061, 89197, 2259, 60665, 66994, 71210, 51232, 3585, 142096, 55024, 7892, 8345, 58653, + 463307, 65658, 64319, 137941, 136323, 53499, 12746, 43492, 6978, 95163, 29925, 60175, 5128, 7352, 41463, 184756, + 121146, 20473, 18426, 4598, 5309, 54580, 14277, 121151, 10691, 56711, 43880, 63409, 76682, 11830, 172218, 264898, + 32632, 66536, 81062, 31649, 25788, 92774, 60222, 11100, 63159, 9432, 224657, 25240, 53613, 152, 138620, 163829, + 2397, 85345, 12501, 37507, 64932, 38575, 43522, 65789, 80198, 78796, 35226, 3851, 108891, 73311, 3060, 28391, + 93671, 39663, 46142, 30982, 66041, 37281, 68157, 26553, 71872, 81142, 211527, 39747, 118119, 22695, 2859, 11066, + 20232, 168911, 7933, 197005, 17066, 111071, 44434, 133994, 120798, 12766, 227798, 45756, 132852, 29917, 36076, 55352, + 65281, 129800, 41958, 18944, 84678, 18580, 168093, 132621, 39997, 54092, 27740, 32354, 3770, 114118, 103242, 43918, + 15899, 18574, 145944, 3190, 123469, 219903, 24169, 100571, 62403, 16776, 92779, 14535, 17168, 16475, 14304, 37231, + 1712, 28218, 242754, 61688, 28980, 1318, 51359, 222657, 99200, 67989, 31772, 23932, 35351, 201251, 49041, 27306, + 19128, 40135, 3986, 77333, 19649, 120683, 151927, 21081, 7076, 78375, 77501, 101599, 8011, 89585, 96715, 58179, + 5378, 102138, 106793, 26051, 217276, 4197, 16297, 27014, 46721, 13322, 22806, 5278, 29629, 70632, 9647, 71519, + 58818, 40603, 128530, 8903, 36770, 56900, 31483, 26935, 43845, 34265, 34920, 87658, 6114, 84767, 64250, 47318, + 50720, 19264, 162514, 33357, 13117, 6705, 46696, 75032, 71054, 87004, 42035, 69138, 11903, 99854, 102328, 19611, + 34525, 69312, 6431, 49842, 101600, 133178, 108751, 41829, 89939, 225664, 48916, 99556, 9195, 130387, 5960, 36857, + 116724, 53518, 94002, 39077, 53996, 6945, 22261, 64291, 8314, 152785, 57588, 16522, 9091, 5048, 87671, 35441, + 39509, 1945, 12423, 158923, 178413, 37549, 14095, 1475, 73188, 62878, 4819, 24012, 68534, 42606, 4010, 120809, + 57497, 59564, 101758, 103718, 32701, 80116, 12345, 95834, 46918, 21468, 53213, 15665, 31200, 3867, 5140, 96013, + 250744, 21016, 10069, 13968, 35449, 180829, 27683, 39704, 59956, 22893, 3115, 26293, 32785, 75934, 62445, 141162, + 62720, 2018, 83638, 19949, 114012, 95006, 3330, 99829, 130935, 309272, 9565, 55874, 121727, 37017, 23586, 319858, + 40970, 27602, 8625, 112329, 61060, 100088, 118525, 25922, 16232, 1907, 60671, 51583, 44553, 80993, 5262, 94679, + 8676, 940, 20736, 11823, 3020, 16476, 12340, 152600, 97416, 3703, 25744, 66826, 16245, 16876, 46446, 84798, + 74227, 176020, 45192, 61955, 75496, 23946, 23626, 40372, 26036, 6149, 11822, 30582, 16541, 41914, 82385, 232823, + 40921, 80773, 14930, 3631, 7517, 39619, 4348, 36180, 126106, 138939, 62611, 1477, 113512, 47321, 25052, 14546, + 118881, 29060, 23589, 128322, 36795, 18401, 137921, 104699, 267929, 36194, 172791, 18113, 4766, 188215, 30083, 332586, + 94089, 5805, 77909, 22194, 68234, 154976, 43220, 40660, 70001, 184893, 138095, 11128, 103010, 22663, 5108, 212615, + 8485, 5565, 49222, 54614, 26530, 42639, 16319, 55062, 152662, 105595, 21114, 22216, 10294, 68158, 10436, 86950, + 7206, 62115, 3977, 3657, 59874, 456, 118617, 18156, 106663, 112229, 80992, 17442, 8217, 55551, 5133, 34344, + 251927, 51153, 39364, 201321, 7816, 66803, 23057, 156724, 145664, 14276, 95705, 979, 2796, 6875, 13429, 212525, + 50602, 26276, 28284, 3424, 19465, 52397, 46963, 31420, 51399, 206476, 92317, 48851, 637, 100820, 83349, 10317, + 60227, 21972, 6908, 282439, 32857, 224767, 95629, 83882, 42106, 87338, 69757, 29840, 68709, 37665, 45244, 114577, + 49188, 175943, 54009, 186746, 106158, 70168, 3358, 234002, 50555, 9221, 129338, 9562, 20118, 32923, 78479, 118280, + 65752, 4977, 10474, 102174, 60947, 129006, 10570, 83451, 8598, 8078, 159367, 123785, 80438, 16742, 5905, 5281, + 181513, 42402, 6977, 163136, 93179, 42191, 14968, 50421, 112401, 105440, 33456, 57347, 121611, 4221, 94954, 36517, + 24046, 27796, 6255, 33394, 72990, 135408, 116627, 1233, 57874, 25654, 95419, 68156, 401399, 313338, 55208, 45573, + 93124, 119251, 47200, 38196, 11909, 130667, 45391, 73904, 64964, 167846, 4137, 115606, 52036, 62214, 7969, 160925, + 7187, 1132, 134835, 40309, 73195, 64494, 80472, 444841, 61111, 26500, 45323, 40743, 53625, 52797, 22659, 15631, + 29739, 36706, 28841, 39147, 102836, 26794, 10536, 14845, 87305, 45874, 12241, 127587, 83833, 57183, 79722, 30844, + 41304, 84655, 20825, 92500, 3722, 25655, 27811, 10157, 81634, 31362, 34088, 92487, 70123, 22190, 185100, 72658, + 139035, 192523, 88241, 2078, 230490, 44528, 85638, 100198, 22088, 29982, 291233, 241062, 13865, 4445, 137791, 37835, + 107218, 31726, 19718, 38234, 72528, 23046, 19177, 66695, 5109, 17251, 28077, 5617, 21554, 47839, 72425, 133825, + 1486, 73065, 181275, 141508, 21768, 62971, 63082, 2512, 34200, 9904, 120309, 6392, 91243, 68416, 268253, 41199, + 116757, 138551, 185526, 41246, 28986, 4093, 19057, 17295, 4148, 245766, 122360, 35356, 112075, 20301, 75441, 10998, + 7977, 19769, 62922, 937, 63547, 100196, 26427, 157820, 20983, 236696, 22935, 8140, 90315, 156004, 47204, 140973, + 7726, 45097, 52725, 22636, 23436, 257282, 105247, 522, 88389, 216031, 202204, 46812, 211666, 19693, 68828, 81691, + 45925, 11256, 30292, 372, 5236, 167826, 88328, 232776, 151611, 5360, 82104, 18841, 80393, 25465, 18285, 20320, + 72377, 31730, 33160, 45803, 38715, 27705, 37379, 24163, 18360, 103586, 4015, 32305, 269494, 91252, 20080, 36567, + 54650, 7797, 57073, 12650, 31164, 42209, 6375, 261663, 105528, 81661, 106002, 2800, 5375, 17247, 43151, 4442, + 15727, 194619, 100855, 144898, 62320, 78465, 39929, 16454, 1967, 28311, 61363, 17219, 9395, 8745, 121445, 76939, + 80385, 162380, 22009, 54191, 44248, 16299, 122830, 48151, 74429, 78291, 64755, 14238, 44966, 2511, 17712, 67954, + 93583, 829, 105899, 49935, 84750, 11591, 33185, 85447, 42717, 27409, 208542, 28965, 62052, 52525, 5597, 25694, + 65594, 16343, 63224, 276188, 12475, 9331, 127507, 38522, 57287, 24128, 133161, 79723, 105548, 133695, 48917, 27558, + 43278, 46520, 13778, 141954, 110785, 83366, 17715, 46317, 105763, 66298, 147013, 41086, 94180, 16478, 220447, 44611, + 730, 19722, 78975, 117889, 125643, 26254, 16574, 18480, 65006, 15806, 38549, 246418, 46052, 36056, 8440, 34984, + 30170, 3163, 59800, 4458, 115442, 4283, 41970, 33507, 104078, 1653, 22, 121158, 276486, 3655, 6338, 24048, + 133421, 23641, 2161, 24422, 36006, 8086, 10675, 181474, 12307, 29514, 59143, 14729, 52509, 87128, 122470, 19446, + 80852, 33314, 24573, 119864, 14237, 9652, 57779, 6612, 51851, 15284, 98871, 90581, 124466, 156831, 21190, 22015, + 71380, 161906, 87247, 69201, 18392, 17908, 108470, 72962, 40719, 14338, 17911, 95260, 43339, 20610, 78916, 20710, + 72451, 11315, 31448, 17263, 58853, 178878, 48111, 116002, 45497, 80506, 82605, 85880, 36300, 121755, 25215, 36118, + 301929, 88728, 405223, 276136, 553, 34704, 212438, 49970, 78329, 922, 20711, 25036, 257130, 38295, 145369, 18128, + 15385, 30829, 55656, 48345, 8012, 3561, 28004, 122041, 192900, 58338, 112508, 41085, 29976, 87040, 47117, 23905, + 4336, 92061, 138880, 97407, 42083, 172121, 6256, 25192, 172671, 5, 93568, 1420, 12677, 31605, 56743, 40620, + 6015, 78415, 231077, 31298, 80026, 13902, 19048, 24924, 170586, 32955, 176119, 87859, 36731, 6773, 27711, 24658, + 26475, 115216, 133207, 93250, 95820, 88522, 8317, 5714, 124047, 55219, 86860, 19677, 23961, 22928, 162209, 8904, + 225992, 359835, 56084, 96201, 29392, 96558, 86071, 93643, 55114, 13347, 8183, 95129, 82012, 2017, 123336, 34219, + 115554, 157159, 47747, 101684, 41008, 18735, 193781, 104151, 226906, 7552, 179874, 124113, 31159, 21162, 44010, 14771, + 51268, 166128, 31382, 73124, 77438, 92830, 205709, 12113, 1292, 38937, 13114, 1334, 2118, 15597, 69581, 14449, + 21934, 76618, 48728, 67038, 14967, 51495, 24243, 87736, 147249, 26720, 11119, 46063, 43749, 5843, 44147, 152629, + 133428, 65703, 14269, 45604, 57982, 28672, 55616, 45957, 8438, 95433, 37698, 220862, 132034, 39456, 61870, 4161, + 26501, 73560, 56418, 9845, 4654, 20916, 10456, 88920, 119358, 9015, 65931, 96507, 48029, 38534, 21676, 109081, + 43078, 34943, 25089, 6131, 28766, 23665, 5477, 10255, 16695, 67, 45778, 42443, 42770, 29534, 23733, 100513, + 62617, 42630, 48746, 14191, 43753, 50295, 26007, 8792, 57243, 43119, 54725, 164253, 58250, 112304, 131796, 25165, + 4651, 3188, 24831, 47748, 3705, 19540, 13211, 102095, 5593, 18699, 23666, 32005, 117571, 33541, 60584, 74573, + 86311, 99443, 25172, 27222, 168938, 7143, 11853, 53560, 18834, 19960, 86522, 28217, 53266, 117700, 72989, 34323, + 18721, 66450, 34346, 74056, 47217, 202002, 46269, 9429, 68582, 75458, 37823, 82843, 96652, 32549, 145144, 27958, + 19820, 158086, 31955, 201406, 135379, 31207, 192545, 12950, 51704, 9094, 248263, 76147, 64028, 110009, 79407, 89345, + 99284, 223492, 47966, 26848, 15359, 201137, 2861, 110507, 71231, 72297, 31851, 118777, 71039, 151051, 240855, 16333, + 50766, 14727, 7939, 4149, 80908, 418780, 88378, 59276, 1327, 7284, 38576, 79814, 65820, 42199, 84860, 49574, + 62596, 12396, 70598, 40117, 8648, 7994, 16836, 7630, 14047, 359699, 106878, 525, 29037, 28064, 13380, 11675, + 50669, 74216, 103539, 180314, 27449, 56299, 172344, 19274, 7301, 246099, 32043, 19422, 36506, 129317, 6806, 30140, + 4614, 46639, 66926, 932, 86600, 6322, 27847, 233103, 10541, 39025, 34887, 3517, 12972, 26220, 2031, 66561, + 115015, 48658, 47596, 12714, 33845, 3893, 16165, 35237, 89983, 14769, 11962, 147224, 47018, 29977, 27979, 5552, + 82338, 86023, 131368, 1218, 24853, 237840, 132193, 15455, 40873, 3668, 65351, 53388, 15229, 59889, 272245, 47934, + 11858, 34347, 18038, 90853, 86981, 300602, 19343, 114181, 29362, 84921, 6095, 106059, 79472, 38015, 1206, 48741, + 6208, 80000, 21916, 17423, 6002, 108083, 24479, 34931, 56661, 9511, 26995, 100694, 163853, 35997, 81254, 58321, + 18919, 171890, 86877, 91341, 74503, 70477, 53412, 7027, 59281, 39892, 131302, 5864, 15947, 61301, 67466, 162369, + 47956, 27874, 35624, 282324, 21270, 111847, 102548, 41482, 30955, 116737, 28264, 8592, 55458, 22301, 75090, 29821, + 30697, 51709, 3041, 19208, 8038, 24634, 30467, 87509, 126428, 19389, 18814, 152686, 20701, 83474, 45832, 80891, + 105808, 11378, 153223, 120770, 98186, 150633, 49838, 9141, 12755, 30962, 5260, 74490, 21256, 31678, 65062, 33326, + 289838, 187831, 20595, 89768, 2805, 58535, 10844, 70085, 12090, 2451, 138068, 98544, 24461, 4511, 6754, 41684, + 28203, 3383, 65355, 82833, 30161, 83924, 234361, 128424, 28921, 222594, 33975, 125491, 34069, 11508, 67464, 144226, + 41850, 98703, 34371, 7901, 21254, 38398, 65651, 23549, 53883, 213340, 123269, 12028, 71764, 177701, 28758, 2623, + 68395, 11549, 15232, 68603, 9660, 63116, 36079, 57093, 31198, 20475, 48467, 89984, 35619, 186847, 107469, 31389, + 43631, 73867, 41949, 68841, 114250, 1605, 30564, 63403, 17588, 27680, 99533, 12641, 70325, 50428, 73426, 78379, + 11855, 91651, 72081, 91720, 60198, 15743, 12065, 83398, 140046, 6761, 46598, 45900, 5068, 886, 62448, 148968, + 37347, 19405, 9680, 15819, 43496, 63370, 75667, 163700, 37639, 3633, 22774, 34341, 183131, 134335, 37200, 23915, + 7054, 14194, 12970, 26438, 13350, 285521, 25594, 8219, 104410, 91039, 168804, 138480, 149734, 15907, 33818, 61132, + 60082, 4622, 110187, 56736, 13551, 73571, 3945, 73463, 65498, 17758, 263266, 17593, 2710, 27585, 54469, 38200, + 45367, 63754, 28881, 3473, 12791, 98287, 31895, 65787, 4463, 94536, 24951, 36332, 59901, 28803, 52130, 86403, + 7668, 181822, 74831, 18977, 9850, 177206, 145485, 109798, 7292, 31421, 26280, 77211, 58511, 12507, 127004, 11113, + 147, 8729, 56208, 43066, 79926, 129937, 31345, 83947, 39915, 46146, 98763, 42566, 1337, 13192, 18323, 105163, + 80570, 117753, 16555, 72883, 11077, 159438, 40764, 70933, 83329, 26066, 12276, 72059, 21655, 173836, 126713, 69454, + 153482, 91585, 70644, 102558, 110483, 6764, 127864, 190133, 3961, 101798, 20945, 71138, 82402, 90884, 69669, 44753, + 923, 16939, 59700, 164258, 25969, 27082, 31399, 43846, 6306, 246093, 51342, 6153, 151581, 202801, 182731, 56475, + 162188, 89426, 141356, 14355, 121815, 27536, 28023, 65257, 77523, 106668, 127314, 24947, 12790, 38796, 169698, 23555, + 10725, 44573, 183083, 42088, 62716, 43265, 105958, 32050, 44067, 50118, 1668, 3874, 6243, 318411, 16599, 1691, + 94999, 52378, 28671, 216728, 123258, 2059, 34969, 69225, 5913, 136280, 171443, 141515, 91662, 22175, 135282, 80020, + 92270, 1663, 4808, 4482, 3495, 34691, 5226, 109830, 108512, 17342, 107488, 11606, 123190, 100247, 29666, 146527, + 113014, 15794, 30894, 13224, 39585, 243192, 22351, 9903, 7836, 47699, 11078, 25468, 122291, 48821, 26780, 122679, + 75521, 81450, 630, 4895, 92900, 55074, 74293, 17441, 3563, 111657, 103102, 51613, 12318, 52370, 36191, 68245, + 34269, 40445, 41354, 122901, 168604, 182500, 62012, 42557, 11259, 24428, 115113, 86345, 12362, 3909, 78430, 86852, + 134602, 20459, 47853, 93879, 22577, 7659, 3688, 38555, 13349, 17381, 56715, 91639, 12493, 10895, 92438, 3142, + 37057, 28928, 2004, 36427, 32268, 34222, 209974, 10432, 67436, 41989, 173518, 107930, 27079, 62729, 30908, 55558, + 5828, 45031, 14902, 53546, 8204, 144263, 60255, 14520, 88212, 86582, 109589, 69356, 8064, 47449, 8505, 66558, + 16886, 4844, 52817, 111260, 215129, 12941, 91118, 650, 20770, 6273, 73089, 40618, 62790, 2873, 35002, 14023, + 97208, 19386, 102646, 36993, 143736, 135457, 35385, 113601, 17893, 32627, 84439, 100619, 56016, 6581, 57264, 172160, + 45452, 111710, 203627, 70131, 24100, 322787, 1996, 35665, 70078, 22358, 90922, 83658, 4097, 63200, 58499, 14542, + 99153, 52159, 6615, 12414, 63415, 31986, 16823, 1579, 65405, 137809, 8841, 16898, 48082, 259, 33014, 42375, + 12260, 179850, 73667, 91389, 98882, 29532, 17311, 326251, 41092, 5928, 20742, 44964, 48019, 43505, 9317, 49265, + 6643, 192712, 48424, 163487, 19861, 20113, 70848, 31928, 105333, 23685, 78563, 14638, 54755, 7158, 24142, 44018, + 20774, 125255, 20331, 24280, 10163, 1285, 2336, 39851, 4299, 117269, 46714, 63816, 87779, 159624, 11731, 9971, + 990, 137317, 108831, 50994, 74554, 162680, 23640, 131597, 146962, 170620, 34829, 91205, 21184, 1913, 63616, 18427, + 93136, 156592, 17519, 67565, 115882, 138220, 78622, 88535, 18115, 2711, 33554, 109492, 54298, 971, 24914, 25863, + 36363, 45715, 27099, 194995, 14299, 178181, 111488, 72395, 322385, 157719, 130787, 11897, 81843, 83999, 11369, 49280, + 118604, 40922, 61332, 110343, 53407, 75639, 40582, 300440, 54722, 25637, 13694, 48248, 48278, 194521, 56203, 52779, + 48783, 72627, 10953, 376, 16733, 280238, 26351, 230789, 15132, 25168, 137270, 3588, 63704, 73376, 94031, 74284, + 19443, 159557, 9697, 39901, 13351, 119050, 15406, 146455, 3460, 29556, 75195, 37673, 102524, 92329, 47289, 98413, + 15311, 100684, 56345, 7116, 95480, 11590, 7200, 167, 23610, 58426, 17730, 136656, 27944, 53151, 2701, 8824, + 103124, 3017, 90744, 113588, 53216, 79736, 65940, 26931, 498, 29568, 80540, 143543, 21292, 1740, 59268, 16561, + 180816, 42323, 50174, 40890, 52866, 10703, 57169, 4700, 17191, 4424, 93511, 49698, 166650, 26972, 48631, 165169, + 82879, 69326, 202970, 4007, 2376, 231325, 139592, 22119, 62851, 37504, 68816, 58345, 67398, 186643, 43331, 277416, + 53749, 15746, 23102, 17432, 4793, 151138, 48822, 54265, 48203, 198688, 14305, 54287, 2291, 18018, 113378, 123260, + 7180, 97549, 87027, 120085, 2920, 76080, 8190, 102005, 5641, 64580, 14955, 59802, 54028, 58884, 19367, 81779, + 412567, 85957, 97053, 103637, 78871, 29364, 27637, 141728, 4767, 30686, 112738, 130146, 42745, 12730, 105040, 14844, + 232, 210944, 36581, 152317, 135543, 29744, 3129, 55647, 58149, 46319, 27265, 17499, 28005, 59948, 7170, 34138, + 5702, 293047, 110892, 408, 91760, 218674, 18469, 46095, 81403, 14389, 4610, 35672, 73060, 11006, 74848, 104820, + 118143, 190357, 20043, 105358, 141735, 5115, 27093, 45924, 123073, 52599, 29433, 9616, 238350, 78610, 24851, 58858, + 26769, 31969, 24613, 18294, 4982, 32735, 39639, 143563, 112073, 202205, 12567, 4873, 88601, 44897, 81503, 101648, + 81362, 34662, 85277, 17574, 48173, 21435, 221188, 40215, 39576, 80786, 26544, 64668, 81841, 10731, 37733, 247986, + 149188, 127703, 495, 18382, 54388, 72446, 43071, 30974, 198723, 89608, 41360, 190, 33045, 8386, 31658, 19992, + 237838, 119015, 137622, 50890, 100913, 6460, 116233, 267230, 26621, 104129, 65114, 14190, 41542, 14888, 85962, 23342, + 23041, 26453, 43725, 71809, 45186, 4770, 46452, 53894, 56616, 221286, 18973, 9038, 109299, 55365, 19366, 26863, + 18808, 60909, 69353, 41738, 83463, 12100, 68561, 72860, 3980, 13796, 49340, 12332, 31311, 27418, 4255, 53430, + 18976, 45523, 510, 14224, 30477, 26581, 4530, 3651, 101663, 139840, 22709, 150861, 31996, 63923, 120623, 262522, + 3076, 10528, 2929, 14672, 130238, 18087, 9816, 121894, 100308, 25085, 55111, 14565, 18952, 53293, 2042, 369988, + 23674, 61789, 133529, 28783, 108293, 35477, 47119, 36448, 71049, 40015, 33055, 78598, 198442, 1833, 159937, 40654, + 77444, 189245, 113153, 8621, 18599, 38553, 35223, 166072, 2375, 11659, 21786, 89523, 6032, 12116, 63046, 159398, + 18454, 3678, 32521, 47626, 11411, 103527, 38896, 42946, 15696, 26370, 10185, 8413, 37080, 165583, 4331, 63555, + 14907, 72220, 50056, 6623, 62236, 36565, 49783, 10049, 17503, 100581, 55951, 146244, 24724, 9626, 17969, 25524, + 109300, 173965, 99994, 101056, 46459, 43647, 53737, 277968, 8347, 123521, 74858, 33829, 44762, 77574, 877, 81377, + 222525, 123532, 30602, 43881, 53145, 2973, 16284, 81940, 61281, 127044, 63620, 9875, 14756, 114829, 19032, 9202, + 52759, 119141, 23928, 120551, 19607, 3599, 33401, 76821, 73233, 117430, 39968, 36539, 7071, 5446, 121735, 194059, + 15206, 45283, 6706, 15603, 65615, 1207, 165723, 92275, 34773, 104447, 8396, 32353, 205240, 164323, 13600, 60555, + 79205, 25532, 22907, 33410, 57480, 107111, 69630, 32137, 47832, 70913, 33161, 20321, 2371, 117348, 10714, 86246, + 1625, 11763, 17900, 268, 78457, 99175, 97940, 101092, 86660, 32221, 14041, 128504, 125080, 53744, 124263, 31017, + 13897, 403, 31859, 21964, 5633, 111630, 5547, 77329, 17961, 18241, 84995, 25984, 12983, 67491, 62168, 47262, + 5241, 297, 51191, 7351, 8967, 147212, 82060, 16821, 782, 11033, 82431, 62957, 5026, 43459, 77963, 203477, + 53528, 6247, 191852, 87774, 74164, 215654, 13467, 1522, 219964, 28589, 244104, 16242, 117821, 67725, 72570, 156792, + 17186, 15979, 26990, 44128, 193014, 35276, 57125, 16212, 166451, 68017, 6905, 77608, 16364, 53777, 75921, 76426, + 37975, 26203, 269296, 64099, 84122, 12077, 38533, 830, 4407, 20139, 963, 43028, 38902, 42911, 37503, 83343, + 85045, 16979, 1165, 60835, 137387, 58380, 86990, 110066, 134540, 56331, 193845, 81238, 17922, 163093, 38744, 110641, + 12502, 56404, 34862, 26865, 125964, 12965, 111648, 25547, 7771, 27196, 136980, 9555, 29551, 107158, 57885, 18831, + 37705, 35505, 101742, 13970, 102109, 62548, 124657, 23328, 11124, 89592, 146376, 248050, 6241, 22033, 18337, 80685, + 29898, 11908, 216623, 67721, 106162, 146610, 21377, 15085, 91552, 42041, 62560, 122532, 125336, 102365, 121537, 142559, + 29693, 223919, 11515, 110495, 18776, 22494, 5895, 185059, 103592, 229351, 51220, 100102, 37027, 257855, 29359, 54123, + 36066, 106493, 12244, 79258, 32002, 432, 56205, 94836, 90182, 6726, 14762, 29391, 48938, 26864, 38083, 60364, + 3310, 60192, 14766, 205567, 57504, 110760, 22649, 24666, 46333, 21517, 3430, 13135, 28873, 27052, 158809, 11597, + 20529, 6695, 23138, 22960, 37137, 45574, 6545, 305877, 43423, 26153, 24769, 59844, 14501, 10430, 134352, 56169, + 13213, 103432, 49523, 35181, 13435, 12408, 129475, 64620, 230854, 77390, 51990, 15653, 83248, 33466, 44571, 117828, + 51481, 2187, 10559, 68019, 18021, 54895, 48247, 18354, 33737, 4554, 108595, 37288, 39767, 116707, 9175, 3726, + 108877, 21616, 83684, 49862, 1938, 8543, 276466, 20134, 108498, 48770, 102254, 31914, 131520, 185291, 100559, 51890, + 209, 19526, 76471, 50544, 71814, 99351, 8172, 198526, 28816, 20419, 9109, 98389, 136777, 76479, 75596, 30635, + 165417, 48216, 120220, 25955, 211071, 39314, 24308, 32164, 2559, 146280, 43403, 9233, 17947, 90585, 1786, 86920, + 125662, 2457, 64741, 32152, 32918, 122882, 78538, 44001, 31723, 56426, 23375, 103172, 88177, 145697, 52506, 49319, + 68016, 31664, 41488, 18486, 110400, 7030, 28241, 986, 109199, 19900, 42147, 56864, 65287, 49183, 7858, 24000, + 30453, 840, 16673, 25907, 68916, 89927, 6309, 158335, 36407, 199737, 130464, 13137, 59603, 201778, 195292, 21015, + 42466, 179062, 172561, 89492, 11075, 180407, 31868, 72493, 20998, 60217, 9865, 19530, 39274, 130266, 54539, 21623, + 12535, 13505, 40641, 73375, 4087, 85633, 2153, 3117, 70680, 55788, 92096, 47509, 98493, 37490, 271936, 151475, + 3032, 16171, 96642, 34106, 78425, 125761, 19591, 3366, 19316, 54508, 24183, 50786, 194248, 91528, 33253, 34622, + 108355, 41741, 705, 3814, 3883, 108929, 13203, 67831, 10142, 59754, 68208, 29128, 84820, 56880, 38794, 24972, + 48571, 40821, 40476, 18137, 164254, 24064, 236309, 79181, 11282, 395, 39169, 2013, 51587, 28551, 9645, 701, + 109513, 115899, 113566, 12762, 62045, 58322, 103726, 41343, 40866, 244102, 143816, 2490, 70346, 40973, 52618, 15412, + 30720, 104315, 38917, 42027, 93676, 17513, 107418, 20706, 123890, 13399, 97727, 24044, 87962, 65606, 44250, 98044, + 65276, 74790, 101473, 19350, 91570, 1326, 87790, 172042, 7577, 100813, 86896, 85891, 41512, 108130, 27794, 14875, + 71431, 12835, 156250, 58135, 3759, 22476, 42176, 115873, 34686, 56523, 73643, 108505, 51491, 20838, 12721, 32863, + 45700, 29496, 13700, 34294, 55360, 29206, 155942, 123812, 7706, 163234, 203, 132720, 49358, 144431, 8130, 175788, + 35818, 3270, 76832, 25710, 54095, 97274, 28779, 94621, 74396, 19092, 128242, 58067, 20885, 14670, 93255, 15107, + 63291, 23654, 126900, 129421, 59294, 262659, 9798, 3251, 67344, 28600, 44629, 50672, 29072, 26999, 31526, 23183, + 49175, 165843, 175455, 17282, 175411, 32022, 45989, 30298, 90690, 78118, 83156, 23749, 35636, 31317, 7069, 80381, + 94561, 133756, 14960, 97404, 6138, 41065, 78041, 32843, 16601, 34123, 9559, 146529, 123377, 96395, 54441, 42012, + 84257, 123541, 10745, 22139, 106459, 11720, 150883, 172651, 154996, 110538, 4728, 53447, 25704, 2009, 71152, 119354, + 21166, 66604, 1429, 216162, 8637, 122250, 63520, 27180, 29172, 36124, 276428, 107787, 77184, 4680, 14952, 104903, + 24418, 14793, 51561, 52931, 8371, 26342, 48526, 7118, 92066, 67280, 40653, 8847, 34597, 105438, 14198, 50163, + 61188, 146286, 50315, 41205, 170829, 161496, 585, 197359, 95056, 1687, 365794, 91349, 48507, 5804, 49263, 5146, + 104902, 96365, 117343, 132222, 46084, 96919, 16875, 8073, 262381, 79982, 52663, 13928, 16056, 153908, 15145, 109256, + 132308, 18763, 24904, 167644, 13618, 40750, 18686, 147124, 114709, 150038, 52849, 2938, 12568, 48617, 8778, 5459, + 44202, 44591, 74914, 17183, 248689, 13878, 7822, 80060, 23116, 194037, 18487, 2067, 7798, 43077, 33678, 244028, + 31320, 74273, 2794, 19466, 8218, 36280, 183997, 48124, 19416, 29656, 19280, 98734, 7715, 18311, 30701, 133602, + 150307, 126956, 7378, 2933, 79903, 13178, 12593, 86571, 26604, 92446, 13574, 44205, 65699, 427599, 21118, 8245, + 14407, 27877, 47936, 33542, 7916, 26460, 117762, 21596, 37818, 2249, 127359, 209394, 60044, 47677, 308089, 36791, + 154971, 31417, 6998, 150042, 174360, 12255, 43009, 29335, 48739, 3912, 101398, 53340, 2580, 146939, 151295, 45360, + 125275, 15273, 45383, 27456, 48761, 23314, 8750, 60801, 85823, 104759, 27894, 123685, 66968, 39480, 26917, 55290, + 83305, 2696, 98390, 57569, 145853, 340733, 4919, 20024, 52268, 30884, 7413, 203685, 70989, 112855, 4129, 50536, + 349518, 68205, 332641, 159581, 135361, 236026, 37563, 176404, 64899, 6578, 122033, 63871, 1850, 85234, 82089, 66124, + 74145, 121098, 107351, 12687, 36881, 117334, 13136, 14698, 85933, 93866, 18047, 32620, 310, 15094, 46000, 88451, + 23632, 36645, 27940, 87618, 80520, 58892, 20976, 27702, 140090, 96075, 67841, 103292, 238964, 87778, 107338, 17019, + 83427, 67522, 7302, 8261, 47570, 116787, 8730, 80484, 61772, 174422, 56005, 131193, 52875, 14588, 28471, 59817, + 9586, 15720, 158155, 51307, 109734, 15196, 11025, 59331, 3884, 52626, 102602, 84797, 25158, 27314, 4437, 20488, + 76214, 189248, 35023, 114952, 157376, 2827, 62439, 102878, 129749, 36405, 10329, 109339, 108633, 36662, 1254, 13267, + 5470, 87105, 58004, 15397, 10434, 159667, 21864, 52022, 179464, 3013, 32147, 31496, 116832, 18494, 105502, 129227, + 107267, 50033, 13481, 9954, 24267, 22141, 16257, 116154, 36185, 950, 115685, 11305, 176708, 2048, 178671, 112573, + 287867, 162328, 497663, 95170, 50979, 193861, 50987, 30368, 136257, 31830, 46549, 15119, 169876, 23788, 17462, 249887, + 57377, 1949, 35448, 14791, 43769, 210091, 3783, 34612, 282103, 88380, 245190, 5457, 20491, 98908, 11402, 86899, + 117916, 16028, 162584, 60644, 320177, 156096, 31065, 55876, 22000, 77655, 9992, 23397, 13757, 317623, 63978, 215255, + 2443, 17648, 93231, 27388, 104529, 93807, 55505, 140477, 12046, 112040, 70887, 40152, 94365, 112353, 25063, 114679, + 266061, 71248, 119555, 15589, 2244, 617, 14129, 211431, 70110, 100652, 7777, 4383, 85911, 89221, 21010, 120615, + 58357, 86405, 37554, 41647, 18, 15143, 69662, 60491, 14714, 186134, 148344, 42347, 5410, 168175, 44535, 42449, + 343894, 129417, 99682, 20659, 27272, 140483, 63455, 222159, 17536, 13722, 42637, 62324, 11976, 114691, 148109, 2283, + 32057, 182393, 4295, 147364, 33705, 2075, 44303, 30274, 28331, 63740, 69740, 29148, 10346, 44862, 33716, 73937, + 153333, 12930, 38784, 247159, 2515, 41053, 20256, 83368, 256189, 54639, 115240, 5096, 24661, 175419, 153552, 26516, + 141, 138176, 63885, 34115, 47222, 55709, 2765, 28479, 38875, 236608, 12229, 22921, 77291, 54426, 45388, 2860, + 57787, 114579, 295139, 105782, 17826, 71066, 19119, 54364, 69385, 16568, 12323, 28057, 33346, 34919, 124763, 155533, + 101386, 31644, 8627, 49001, 303600, 29868, 63213, 9103, 77280, 71333, 9696, 138789, 37059, 24823, 5057, 21352, + 32368, 114208, 56803, 19424, 10445, 58514, 8661, 209508, 26187, 171838, 10460, 63454, 14016, 122504, 41328, 21329, + 46618, 32493, 38225, 7855, 31763, 7945, 29876, 8734, 6438, 24205, 97490, 139977, 130740, 47323, 33195, 85390, + 57194, 13813, 60600, 21313, 96251, 7699, 27584, 170521, 139271, 1363, 4402, 336738, 129223, 84983, 69150, 13147, + 3590, 163929, 207225, 155260, 55916, 20288, 4503, 8398, 98490, 11773, 27512, 37113, 84976, 86558, 28365, 11756, + 116005, 182148, 13733, 115313, 47644, 67208, 85069, 9347, 14995, 226141, 14704, 101835, 41159, 35314, 13113, 63526, + 214039, 29978, 50446, 83339, 17440, 129441, 72522, 118641, 97816, 24907, 73844, 15717, 118884, 167255, 96509, 162793, + 30847, 36849, 51297, 78974, 77793, 10427, 1873, 2972, 9999, 35074, 28190, 64297, 146836, 46298, 60038, 163007, + 108919, 61219, 2403, 75022, 127339, 4233, 110389, 69022, 9833, 128097, 88016, 79390, 222936, 22570, 94657, 28462, + 56956, 38803, 81536, 30474, 152794, 19566, 16481, 147408, 74574, 81895, 20731, 1918, 1366, 76367, 187321, 54494, + 24366, 21690, 61696, 33283, 107477, 77499, 31112, 414383, 74362, 18463, 218441, 120929, 59848, 258629, 201924, 69269, + 454, 19989, 13054, 59894, 3623, 58908, 20681, 35723, 78523, 102680, 38988, 184112, 108087, 50944, 132704, 52966, + 21699, 18860, 96349, 201411, 82697, 85395, 95658, 5093, 6427, 177894, 44191, 32755, 26961, 155739, 6249, 31310, + 81030, 26574, 84311, 120155, 86730, 113535, 7424, 48888, 13516, 45747, 98098, 20077, 183995, 81945, 43210, 26704, + 40420, 75831, 45648, 11180, 6855, 57927, 65528, 124096, 34851, 2598, 156633, 107572, 127352, 38169, 123845, 60142, + 62722, 105584, 232364, 23211, 68120, 1601, 22169, 89299, 747, 258039, 80572, 7258, 152249, 11862, 101204, 8834, + 121434, 33761, 19175, 133142, 46343, 40178, 48723, 3589, 41977, 30210, 38868, 62257, 10087, 82658, 87827, 90646, + 16415, 47552, 351723, 28298, 72225, 91146, 272760, 1701, 11295, 1652, 109651, 300747, 51863, 198800, 29446, 11794, + 32345, 37538, 22356, 33102, 37590, 113544, 37970, 11478, 179743, 25454, 103417, 59905, 221970, 105196, 145604, 7817, + 164809, 102360, 16974, 75840, 255333, 56902, 6659, 1954, 645, 59400, 67769, 7689, 18675, 5215, 13793, 20536, + 27852, 3387, 29523, 259718, 16860, 94625, 43143, 29245, 15848, 233581, 22685, 63631, 78557, 22836, 133302, 84513, + 1348, 51826, 47129, 98836, 58284, 1830, 1749, 94642, 10933, 6145, 12506, 10975, 13879, 103781, 144434, 10268, + 28409, 32346, 52968, 121567, 107374, 77268, 23686, 35097, 10501, 155275, 15303, 47136, 21102, 168741, 55332, 90385, + 15996, 84817, 681, 137803, 25054, 142275, 6163, 38175, 8056, 124296, 240642, 65621, 4934, 178205, 16101, 62803, + 60964, 18230, 100622, 76465, 44689, 14545, 9543, 47514, 16852, 93380, 28048, 12047, 107106, 37575, 101485, 77047, + 57326, 34819, 96137, 76916, 6469, 46264, 115983, 75768, 87668, 69942, 13027, 165, 8373, 114231, 26434, 52844, + 42799, 182044, 23580, 146254, 38081, 43236, 33883, 146220, 382894, 14606, 46035, 36481, 166621, 35417, 95382, 2957, + 59384, 60428, 36358, 66343, 75378, 22267, 22950, 83528, 17577, 56474, 25285, 4619, 179691, 75355, 95836, 53295, + 34588, 171410, 4487, 14679, 84208, 44015, 18562, 109133, 54101, 11531, 86052, 174479, 303157, 28095, 9953, 35642, + 14564, 39802, 16145, 77606, 117406, 53038, 121117, 53624, 22062, 1212, 7632, 127157, 237292, 189087, 10478, 127345, + 102515, 181997, 86752, 87623, 10966, 121602, 68783, 68681, 83042, 114380, 138349, 191305, 67176, 50085, 39016, 1427, + 42384, 1412, 67118, 122616, 72389, 25260, 2237, 13576, 137346, 19938, 20304, 2191, 68759, 5373, 61364, 238507, + 75814, 23931, 69565, 38993, 131741, 38364, 12528, 87762, 5679, 129853, 5310, 186831, 32653, 90338, 260176, 389531, + 108118, 26843, 43985, 50175, 30563, 25106, 56965, 18130, 140428, 4542, 165503, 117991, 24219, 229605, 1819, 129663, + 1240, 3797, 76093, 18398, 71339, 51919, 93043, 27175, 47060, 216257, 6483, 35051, 1217, 16512, 80798, 129064, + 13225, 69339, 8548, 237079, 72298, 2575, 34280, 51379, 117910, 55671, 53345, 247552, 29486, 39328, 140821, 34681, + 57045, 60177, 5004, 90269, 78522, 2479, 322607, 48474, 61296, 13057, 31558, 4678, 59271, 6699, 27044, 31988, + 35944, 12503, 83480, 4389, 136508, 3781, 114121, 70279, 4488, 155829, 42214, 2898, 68191, 75695, 305850, 45041, + 74344, 106509, 30087, 17429, 93292, 12477, 290, 23080, 114802, 35714, 18751, 26554, 105424, 17775, 2144, 2412, + 100610, 65192, 113975, 52975, 180272, 135050, 129815, 76238, 106483, 21440, 63186, 4260, 46189, 9711, 28249, 4169, + 23429, 23390, 8324, 141585, 63809, 67668, 38457, 38063, 39226, 59972, 1189, 203916, 62368, 14403, 16949, 61767, + 85801, 1739, 40147, 35049, 76757, 33124, 62102, 15780, 103593, 103009, 53484, 22952, 67973, 114645, 6566, 5245, + 50462, 7601, 8288, 3513, 194571, 80276, 1908, 54592, 5124, 58571, 2513, 6800, 273997, 193904, 1119, 17991, + 117245, 2508, 129156, 82366, 26278, 71465, 63341, 56943, 39662, 106116, 94966, 156875, 9736, 2204, 122308, 94418, + 27134, 1280, 24539, 49022, 45314, 3764, 50904, 46424, 30699, 28087, 293839, 9400, 33646, 40165, 822, 147499, + 50263, 116179, 29085, 11863, 31314, 5578, 17797, 5104, 12454, 1604, 15342, 219206, 10232, 67800, 94261, 25872, + 13565, 90339, 78971, 75377, 26649, 41184, 47695, 11514, 35369, 20767, 14227, 41953, 309396, 148270, 147938, 33074, + 14453, 27499, 109019, 39018, 25738, 240196, 158931, 52820, 8612, 95853, 21524, 137010, 84901, 70869, 70021, 116794, + 48404, 38771, 6732, 1070, 70990, 187297, 49140, 5238, 576, 3564, 253975, 16027, 16483, 2811, 37775, 19034, + 25259, 4053, 2000, 70083, 95774, 19713, 33431, 92703, 91314, 42381, 288770, 48194, 95985, 3991, 77418, 13406, + 241328, 245086, 56533, 35275, 62725, 9246, 51924, 70181, 95331, 16163, 31410, 79016, 39312, 120878, 119371, 275987, + 80124, 27712, 9186, 220, 23598, 146167, 85209, 68238, 282190, 57048, 31273, 30555, 80913, 17594, 75779, 59160, + 135002, 101219, 189377, 29225, 96735, 60126, 62522, 104000, 27620, 86814, 17240, 147533, 11001, 5425, 43682, 410, + 49460, 87270, 69480, 46315, 59448, 1816, 76201, 9431, 11788, 87960, 29063, 65539, 47347, 11678, 33846, 7008, + 196704, 9895, 6753, 8633, 120892, 59970, 572824, 115934, 6646, 202559, 892, 48351, 37611, 251282, 57823, 67263, + 57750, 26527, 34485, 90747, 7685, 88370, 6144, 64182, 1709, 41969, 21458, 62327, 181657, 49247, 225330, 122600, + 114574, 107124, 85361, 111833, 63243, 71420, 15655, 191178, 72430, 18063, 51425, 54002, 12364, 53225, 86557, 18193, + 97580, 41232, 138398, 67821, 128724, 8944, 233212, 101353, 52099, 42127, 14006, 120107, 32789, 32132, 3498, 18123, + 33758, 56058, 5779, 128760, 59888, 98869, 18445, 84702, 51911, 13234, 218379, 20093, 39031, 8074, 70195, 20708, + 23462, 24355, 131384, 60189, 26390, 10403, 41060, 7140, 10781, 49410, 42261, 87202, 82566, 41663, 43105, 60276, + 2768, 5733, 74176, 28329, 2297, 145430, 131632, 83615, 122915, 105441, 655, 224102, 5284, 136426, 67763, 16294, + 188511, 32538, 61049, 27893, 3394, 13951, 159099, 28542, 17930, 145360, 9492, 190122, 32285, 78855, 26440, 13570, + 58648, 73908, 4239, 124561, 2444, 74172, 53131, 11468, 10794, 73566, 11623, 35343, 64710, 30481, 4163, 10328, + 38309, 29901, 10538, 154377, 76132, 92405, 24839, 11679, 3465, 13449, 11637, 7824, 2337, 57754, 1260, 14458, + 41118, 19878, 38661, 13416, 159180, 37074, 163164, 54137, 28627, 52134, 184900, 8520, 40385, 29546, 30502, 22386, + 66527, 107458, 6850, 24022, 47983, 30603, 35083, 8934, 304066, 39500, 9, 28261, 33026, 77251, 9374, 44833, + 116312, 34990, 29236, 63563, 125639, 135405, 165398, 159055, 55690, 88141, 69643, 236964, 31983, 25572, 20436, 36746, + 60896, 31850, 16179, 11828, 5888, 3043, 66368, 9750, 31167, 7915, 53111, 36430, 1333, 64344, 93659, 20061, + 60596, 180191, 51630, 6792, 30244, 43509, 101058, 22409, 420, 44210, 109783, 43223, 27030, 72477, 72831, 32679, + 29235, 7675, 47556, 12258, 39907, 149412, 84926, 118247, 24692, 71717, 105038, 86009, 45941, 41189, 89453, 29856, + 52543, 30627, 226798, 67303, 59230, 67415, 34408, 1367, 99685, 16867, 128419, 52147, 4111, 125381, 117881, 16173, + 44093, 102224, 31575, 23234, 24870, 83790, 127407, 239098, 3200, 994, 1255, 100903, 242275, 117266, 55116, 38205, + 16140, 29662, 11307, 40414, 208793, 123355, 56470, 4862, 75600, 30119, 58218, 70828, 24075, 26974, 7802, 192353, + 4851, 5475, 78720, 66596, 3409, 28573, 64396, 30381, 30690, 59859, 88256, 5406, 99945, 103064, 34463, 37727, + 24238, 86643, 60088, 4057, 23741, 5967, 162904, 38240, 28356, 93858, 25510, 122879, 6897, 3278, 7057, 11971, + 4400, 35461, 211413, 21395, 59615, 39471, 87233, 55795, 128426, 3051, 22470, 41950, 14705, 3974, 180108, 80476, + 78442, 204996, 91987, 15634, 67610, 139015, 142373, 35611, 51134, 10387, 4353, 153456, 57749, 181039, 14183, 68447, + 151532, 21107, 36452, 20551, 3186, 46247, 46383, 129666, 88736, 140662, 146243, 2066, 8360, 7978, 64818, 106963, + 17896, 47801, 10723, 114821, 223295, 74192, 3293, 3393, 16987, 74064, 11277, 91622, 4270, 29828, 27951, 387869, + 103235, 1374, 61988, 120083, 477, 145892, 128378, 11779, 211263, 61354, 18221, 17869, 46530, 83061, 108538, 157981, + 90608, 67199, 95080, 49064, 195814, 12302, 66307, 10348, 231346, 160732, 112859, 63633, 146558, 21271, 31037, 198802, + 47622, 12862, 95710, 3910, 77850, 73961, 85585, 34752, 61000, 4082, 24595, 103679, 71107, 8208, 79568, 150019, + 16615, 24961, 139857, 32664, 197366, 4559, 54735, 32696, 4126, 162019, 75698, 13916, 70108, 159638, 19834, 9349, + 24675, 175560, 49643, 18206, 52459, 27992, 10809, 88865, 401975, 133172, 29000, 34558, 30915, 3658, 25834, 42430, + 36562, 125265, 18182, 10155, 40149, 97082, 208980, 19575, 60853, 90529, 66545, 9600, 789, 46420, 2317, 88593, + 55595, 98980, 115302, 5742, 169155, 1073, 177901, 3472, 11189, 63711, 78643, 65472, 50459, 127979, 93, 42202, + 67053, 21720, 157650, 11145, 141378, 42033, 22824, 85705, 79114, 35584, 15974, 1510, 54172, 28562, 12451, 104226, + 19190, 97151, 73024, 20948, 5151, 81741, 21499, 29006, 84183, 198074, 54003, 45120, 170125, 26240, 35177, 28389, + 64863, 79974, 60778, 176915, 232183, 45342, 2038, 80253, 41564, 40703, 32689, 5430, 100689, 5366, 23007, 134279, + 14266, 26712, 73993, 24934, 64242, 52113, 102887, 61801, 46415, 201049, 54251, 62133, 122757, 164883, 30815, 139966, + 2319, 30842, 766, 13362, 10287, 134518, 86111, 81665, 82440, 28333, 43019, 18963, 8804, 161944, 23439, 102144, + 101145, 80029, 39052, 248708, 30350, 117340, 11878, 128467, 974, 138625, 63961, 5237, 74778, 61834, 67040, 43814, + 13690, 65947, 33809, 232476, 115258, 181745, 28824, 94013, 9510, 10246, 93722, 81976, 7217, 114383, 3493, 16014, + 69045, 72692, 12145, 80981, 9507, 6692, 1620, 60820, 330444, 35474, 33962, 4797, 7053, 295463, 46445, 27026, + 12491, 77988, 49524, 35675, 90947, 29114, 166705, 101385, 133782, 32704, 6186, 84595, 176031, 185623, 45966, 151302, + 63069, 1699, 107491, 947, 15458, 74452, 196212, 6046, 10498, 12163, 10239, 35191, 243951, 9277, 9090, 29539, + 54460, 22820, 26514, 112549, 60372, 51753, 48756, 21812, 70861, 260326, 41, 44222, 10441, 16961, 48148, 138771, + 216194, 5914, 52153, 53400, 212036, 56519, 26245, 10117, 45888, 15294, 138019, 90913, 26368, 43842, 42111, 23348, + 6082, 194845, 161089, 156206, 51546, 11647, 30759, 302912, 262094, 8635, 78876, 26535, 35283, 54183, 31183, 85484, + 147873, 12989, 5197, 6356, 72894, 65347, 20150, 27370, 73787, 1493, 45918, 12366, 190217, 20724, 13858, 10981, + 67449, 81213, 7553, 14115, 72242, 271517, 11842, 48310, 88743, 143726, 22177, 3290, 243231, 58452, 62937, 12592, + 1654, 40066, 33477, 13751, 9921, 128442, 15868, 7106, 75236, 83773, 10775, 36938, 10482, 170465, 17368, 17469, + 161508, 32752, 98340, 800, 19824, 264456, 3901, 87319, 2867, 26782, 9630, 113102, 185815, 24197, 44584, 86366, + 40224, 3636, 140916, 31731, 267731, 9567, 53678, 72984, 29389, 27963, 17106, 50282, 284911, 60170, 8322, 12608, + 23374, 89652, 5268, 39044, 229766, 8869, 151350, 31436, 177342, 12269, 183212, 120418, 116270, 2843, 78888, 69192, + 7865, 184099, 1086, 129897, 18383, 70508, 20242, 18508, 229924, 124569, 35749, 50589, 55626, 9884, 83115, 40971, + 30671, 18135, 14452, 38861, 17844, 201826, 5549, 26413, 17189, 13561, 38539, 10679, 143331, 3314, 36785, 171194, + 49685, 187713, 67506, 4618, 104039, 17060, 195080, 50648, 33159, 19238, 67559, 134840, 28599, 157523, 17130, 38064, + 117398, 94355, 31918, 13575, 34538, 40326, 13997, 3494, 348283, 62481, 26862, 3603, 104426, 244363, 153709, 112487, + 304612, 199674, 41239, 35545, 54869, 293005, 28223, 26277, 26899, 4533, 18518, 15492, 38587, 80488, 70485, 160395, + 263, 60162, 11382, 222152, 4696, 250751, 51921, 182609, 10707, 48463, 46243, 1227, 49111, 111564, 46502, 33342, + 56846, 68541, 63559, 858, 139927, 16654, 229375, 76759, 26478, 33205, 95828, 23399, 92945, 2637, 35630, 28470, + 143992, 50214, 14174, 21456, 166191, 65665, 1711, 21594, 78019, 97599, 111701, 36, 147151, 110246, 189022, 43021, + 30397, 40757, 131935, 42065, 73335, 48039, 26596, 28984, 15102, 2361, 7421, 202167, 69744, 43766, 52826, 3642, + 83304, 33873, 75140, 63169, 192389, 36551, 92748, 13039, 123959, 233220, 21738, 84447, 77230, 20228, 187852, 19095, + 25799, 92136, 108774, 29237, 53947, 2299, 118106, 2687, 8830, 42331, 202924, 33667, 2023, 73763, 30704, 19363, + 19779, 16737, 35629, 48081, 24068, 101013, 162338, 291912, 13749, 24745, 328289, 167679, 70086, 48299, 23306, 16732, + 17801, 43322, 54589, 3586, 63653, 43624, 53474, 925, 109177, 251316, 43805, 13082, 19511, 86565, 142182, 92461, + 17117, 101033, 103319, 64589, 4022, 4351, 235897, 5352, 82705, 107142, 46391, 156084, 5860, 61365, 10558, 13045, + 7717, 18357, 33922, 12590, 33065, 6928, 46993, 783, 46937, 67846, 8952, 26295, 6107, 119656, 18799, 17458, + 50747, 4229, 179559, 112727, 118080, 20683, 41464, 125468, 51560, 49749, 44231, 7359, 35339, 62988, 136487, 67015, + 5208, 29150, 24956, 105186, 48858, 6143, 18097, 6972, 16404, 73489, 58742, 97196, 36357, 164616, 5834, 32267, + 13746, 147733, 15113, 132091, 34127, 106298, 39729, 106426, 22294, 9780, 15602, 36213, 71502, 42808, 66802, 599, + 60755, 5851, 39120, 67363, 108623, 126368, 72770, 91263, 32486, 30596, 151717, 7951, 52002, 43103, 11768, 68942, + 40901, 39344, 24037, 127500, 116890, 48403, 16926, 86750, 17745, 48648, 159545, 34460, 58419, 5634, 114317, 67865, + 31462, 23352, 24010, 98185, 125708, 69686, 68337, 13610, 26271, 70691, 2980, 4768, 27225, 102402, 75453, 28106, + 8104, 6931, 1176, 6274, 6475, 112635, 22498, 6176, 238686, 26832, 28893, 90319, 14441, 15682, 15087, 39517, + 45270, 109134, 104440, 45965, 47645, 81772, 7876, 52683, 87720, 12898, 4505, 185665, 2769, 113401, 15664, 57592, + 105229, 137381, 97059, 119268, 6876, 43309, 33886, 128363, 35476, 144249, 67013, 143587, 83367, 25703, 91436, 59347, + 53236, 2289, 16519, 19844, 46309, 58558, 99834, 23313, 218816, 231303, 36388, 51333, 183535, 109792, 139277, 54306, + 90139, 18235, 8275, 32710, 37677, 82464, 86025, 92204, 88842, 117723, 37570, 128723, 234242, 76350, 73795, 34896, + 148247, 58424, 11105, 11744, 45746, 63372, 17118, 49772, 199520, 81902, 38004, 22911, 33752, 3125, 1995, 53792, + 4689, 26909, 108150, 146062, 69674, 41811, 161444, 84855, 8999, 28561, 16731, 93937, 3189, 21967, 24890, 22943, + 1356, 145300, 51569, 28802, 517, 118679, 31703, 40607, 48098, 108854, 25003, 10233, 73969, 177495, 5248, 24516, + 215347, 146192, 48712, 60626, 69188, 40735, 5866, 586, 101541, 6509, 47590, 52129, 5969, 222045, 110933, 25733, + 24223, 65339, 62812, 2414, 155418, 35819, 16022, 78423, 43138, 20995, 128255, 240673, 46745, 236093, 72176, 57085, + 97841, 61248, 107, 36068, 193177, 105427, 55726, 215229, 20446, 47228, 100420, 87091, 14429, 121708, 23605, 21157, + 187721, 21880, 2997, 203976, 99166, 95068, 25877, 7724, 98925, 83401, 4829, 13182, 18229, 13718, 239662, 38653, + 116505, 153497, 30589, 89029, 38962, 181302, 43853, 78872, 180301, 4786, 248240, 7401, 106136, 112590, 77745, 19731, + 60880, 77789, 125748, 135487, 5975, 48627, 34084, 12419, 215770, 47557, 254582, 10364, 106495, 21856, 67539, 88981, + 38805, 21428, 48732, 42316, 12149, 16078, 52808, 25327, 51322, 33850, 51147, 12253, 122354, 46077, 56483, 254553, + 115417, 81834, 150991, 94662, 86668, 7381, 12841, 100650, 18218, 15741, 22372, 68294, 50705, 15535, 84660, 61887, + 22553, 72299, 31361, 24824, 17743, 46820, 64288, 31582, 77006, 111674, 116384, 30760, 80920, 86149, 77192, 51979, + 79691, 60342, 122805, 103800, 240873, 160744, 233114, 78962, 54920, 8608, 3484, 316104, 72548, 24337, 5088, 230040, + 21926, 10172, 36838, 26, 86221, 83458, 102176, 12062, 17571, 41929, 41170, 28428, 68239, 41750, 103930, 2634, + 18313, 53019, 34825, 97837, 63115, 24606, 73157, 152474, 14715, 91439, 37033, 109806, 140259, 30668, 174760, 380, + 135597, 95673, 136073, 65073, 134249, 13829, 17279, 122305, 4420, 46444, 10237, 64848, 203623, 70728, 10349, 182885, + 65075, 24519, 25783, 40318, 34139, 22222, 63394, 55266, 102764, 41422, 20126, 65100, 90408, 53640, 35128, 48932, + 11192, 38935, 96839, 34782, 39492, 19396, 41332, 6250, 5511, 19492, 51304, 25936, 104466, 54099, 73771, 86115, + 5080, 7669, 30891, 111700, 13931, 25276, 72289, 135447, 14820, 258641, 25265, 31005, 281179, 75286, 393, 95359, + 14623, 13584, 6680, 101227, 80173, 44933, 76666, 54542, 13244, 39348, 458, 25379, 109451, 134348, 81143, 6959, + 65554, 12027, 51311, 8716, 57589, 140731, 28467, 23316, 17272, 30458, 25980, 55229, 77197, 83798, 28302, 114784, + 7428, 34548, 26241, 14712, 39336, 103304, 18928, 54080, 12870, 334, 87722, 15208, 16895, 142098, 114262, 39820, + 83913, 57817, 28682, 7721, 14900, 108672, 11250, 62246, 42849, 415188, 1724, 26555, 24549, 25505, 26443, 107450, + 145899, 61035, 43528, 6901, 60726, 65906, 267741, 21338, 147590, 42079, 18924, 73017, 135236, 15393, 5206, 4026, + 84185, 1531, 5988, 113890, 82647, 303391, 7386, 69844, 71611, 189865, 76523, 31877, 13315, 19314, 198575, 32821, + 1928, 67641, 25913, 104475, 103489, 3297, 70391, 18406, 15446, 113347, 19295, 93790, 27856, 1792, 167471, 116449, + 8541, 4408, 41757, 63233, 25765, 86680, 64501, 27034, 24816, 34975, 6079, 4486, 49693, 36229, 16917, 21581, + 62426, 27862, 11612, 54284, 35702, 194034, 355, 24277, 48262, 87411, 70504, 310164, 118018, 12516, 47559, 43502, + 57433, 107139, 9290, 66533, 80863, 14634, 34312, 91725, 28606, 21342, 67241, 72355, 43244, 375789, 37402, 174015, + 105070, 8342, 44167, 67494, 1890, 16365, 11723, 271002, 1865, 47918, 8350, 45564, 27742, 25110, 125803, 8553, + 49504, 81925, 62211, 4534, 15491, 19011, 80373, 206920, 667, 102405, 128623, 245524, 5553, 113309, 192739, 65766, + 19567, 22832, 261958, 29679, 21293, 71134, 20962, 105123, 24721, 860, 21752, 33448, 18372, 157167, 94822, 35770, + 173224, 232737, 75729, 28937, 46828, 28062, 25453, 5207, 140366, 36665, 30652, 6169, 67920, 150458, 92040, 23186, + 184604, 92330, 20891, 176492, 49427, 27828, 38305, 42495, 143982, 49560, 25503, 90043, 29747, 65328, 47830, 12932, + 11068, 77721, 9003, 25213, 94205, 140426, 46090, 89945, 138173, 192691, 33329, 112232, 129905, 35709, 27514, 1841, + 19957, 31411, 127476, 53572, 17497, 173549, 55063, 175135, 19841, 69314, 5192, 237921, 117660, 150697, 4060, 273045, + 50414, 98940, 65348, 153665, 164423, 58804, 156695, 48994, 213928, 86036, 28608, 8355, 39574, 34540, 16927, 135680, + 18374, 151587, 10830, 53805, 16878, 16623, 4282, 48030, 8537, 14986, 46102, 13062, 72897, 72, 33050, 108227, + 39451, 45935, 651, 113320, 40535, 95176, 57450, 48843, 5003, 19019, 10407, 211163, 3848, 1068, 4988, 32091, + 30095, 41692, 15099, 43602, 107434, 50744, 7627, 171349, 16313, 150832, 352665, 207750, 33937, 38256, 51091, 156000, + 87889, 90663, 84175, 24908, 114900, 50365, 31494, 83829, 5398, 169342, 47521, 54818, 18935, 8356, 43094, 41212, + 174536, 10082, 92550, 6678, 60614, 23355, 69721, 14796, 34149, 128830, 58187, 3179, 208, 40325, 28399, 225029, + 401412, 51150, 31580, 207268, 6657, 10993, 69818, 64282, 289845, 23308, 12961, 38447, 6681, 52944, 31855, 2572, + 47646, 120728, 179148, 37240, 45196, 218274, 4816, 3695, 21961, 50084, 35209, 18073, 51452, 27004, 6100, 33941, + 1377, 84831, 171214, 85, 141510, 9078, 99227, 32610, 6417, 11718, 49868, 65579, 87902, 73018, 49062, 46280, + 61742, 21512, 40862, 107733, 15941, 29168, 157765, 144919, 14487, 5767, 158014, 140070, 7241, 573, 71584, 16921, + 223566, 40331, 179473, 35081, 47926, 140885, 41508, 52104, 59180, 42310, 32811, 29048, 123517, 102413, 80208, 10104, + 14746, 12649, 153641, 126022, 37965, 113017, 4171, 83, 142592, 2809, 6362, 50416, 71323, 116894, 260776, 16204, + 1524, 5760, 30351, 12658, 20703, 54403, 36083, 45408, 74772, 4946, 14485, 50759, 111222, 10890, 2195, 167147, + 92962, 130534, 16283, 177256, 35016, 15472, 210156, 151187, 73922, 117691, 43250, 52051, 37392, 24811, 24358, 30830, + 5775, 818, 21969, 1476, 127322, 151783, 58392, 31021, 106913, 65215, 89407, 90802, 28531, 11690, 20234, 95249, + 44602, 37256, 18707, 11928, 5161, 4410, 26571, 51903, 49768, 22008, 25252, 65780, 209499, 68769, 203726, 13249, + 137363, 48845, 86823, 6658, 5674, 31881, 1083, 1823, 108676, 34518, 166752, 13791, 14287, 91576, 91429, 8665, + 11529, 26401, 16191, 91972, 30964, 5254, 28486, 54697, 79613, 66520, 18447, 22870, 45203, 194466, 22822, 51703, + 12278, 76716, 44595, 73455, 33546, 12235, 144843, 36154, 51247, 11116, 33040, 3180, 225753, 60864, 1972, 28469, + 12891, 28879, 10338, 144157, 56294, 353058, 38302, 41447, 87532, 110616, 27065, 168438, 6557, 1213, 50804, 144643, + 24817, 2390, 136531, 38174, 247513, 16190, 4059, 122791, 131994, 137430, 39506, 57650, 16305, 5188, 54309, 106128, + 20628, 88071, 67394, 395446, 250285, 66176, 91254, 1399, 114196, 43915, 60230, 44853, 27206, 106353, 43013, 18733, + 345105, 226453, 51202, 16607, 57106, 117175, 35492, 10476, 89598, 127439, 15187, 39624, 13688, 61570, 10615, 31111, + 59370, 6238, 175252, 32143, 224492, 41388, 95408, 34384, 148238, 78307, 38959, 9340, 160091, 61443, 15737, 11216, + 41244, 170, 38299, 102443, 113097, 26382, 14027, 33707, 3957, 76300, 66160, 19431, 18900, 6952, 1717, 108656, + 82206, 188021, 257335, 27295, 43999, 41210, 31777, 46956, 57457, 12657, 11489, 15697, 48060, 204748, 53583, 82422, + 284790, 30503, 137341, 8120, 19615, 220311, 15991, 10217, 63424, 9808, 67431, 70976, 98221, 4491, 15177, 28535, + 144789, 751, 13230, 2394, 1504, 33977, 132104, 30316, 22230, 931, 97193, 185240, 24826, 22687, 174322, 15307, + 22988, 1390, 188745, 180325, 29580, 59068, 74903, 18994, 29195, 79, 15436, 7622, 38462, 11566, 138710, 44828, + 45774, 37768, 99236, 68137, 84083, 19282, 22698, 17134, 74807, 126662, 173497, 46248, 16938, 119735, 3212, 28292, + 213652, 49013, 9975, 32180, 45660, 86250, 4801, 68788, 95490, 77482, 113751, 11994, 44624, 94452, 46839, 128497, + 100316, 5798, 58588, 73184, 202987, 65417, 37790, 88524, 1606, 43156, 97964, 105717, 34947, 11203, 100060, 37742, + 130074, 93653, 107799, 94311, 196106, 41347, 8035, 10780, 16390, 27883, 118236, 167395, 1979, 25006, 19375, 31628, + 18916, 144723, 78502, 114047, 103107, 86492, 107686, 5844, 20934, 206963, 23556, 22591, 16562, 146333, 20167, 10471, + 117434, 33085, 2863, 9740, 36669, 41849, 37271, 22790, 18209, 28979, 8231, 12952, 54408, 21731, 25130, 45208, + 55748, 138120, 75826, 414, 29593, 9925, 292865, 25999, 683, 123149, 7036, 92159, 86055, 61827, 103680, 23176, + 54918, 58466, 57578, 13305, 5709, 86479, 16697, 31064, 17660, 200919, 10770, 49793, 33423, 32370, 52047, 16488, + 62555, 6459, 8426, 83493, 7763, 59725, 82812, 18628, 67760, 79405, 68557, 9612, 7673, 28102, 56517, 69620, + 171797, 32458, 29541, 15870, 81109, 32080, 207644, 71495, 21202, 11039, 91036, 61230, 2810, 130800, 32260, 4613, + 60590, 37112, 75214, 33979, 126402, 155062, 30642, 63875, 12810, 194463, 82799, 47664, 16725, 36685, 43367, 61099, + 449, 172150, 102867, 21691, 301838, 36745, 7130, 18671, 57316, 34852, 38034, 54182, 35578, 65900, 99486, 19771, + 3456, 2658, 16914, 99866, 28390, 28109, 8262, 21147, 34353, 20006, 4228, 137085, 1675, 203023, 283196, 198286, + 214375, 163329, 290603, 152574, 40471, 83506, 30068, 14730, 23177, 131539, 34759, 27668, 32178, 71896, 104799, 116305, + 85430, 119262, 42860, 25160, 8911, 23428, 49437, 105322, 6519, 16203, 6349, 74711, 1230, 38045, 8540, 75165, + 44736, 25909, 51026, 317034, 4984, 32281, 91312, 27060, 44431, 17817, 45363, 155937, 239085, 35697, 59784, 91993, + 29531, 126740, 213757, 76560, 167776, 285273, 24262, 8237, 65030, 41160, 74437, 48804, 118916, 13159, 37842, 1031, + 75349, 1478, 11655, 108777, 23435, 277425, 101734, 67469, 70231, 124711, 43532, 28514, 65526, 54956, 1000, 21882, + 17728, 25302, 40952, 52214, 149632, 1999, 2111, 3259, 63362, 89961, 220561, 39777, 26335, 9063, 10572, 12416, + 34551, 34623, 38604, 24723, 5947, 15588, 69927, 66252, 119177, 69173, 46629, 28714, 70715, 212408, 20521, 406913, + 74380, 11716, 50659, 50862, 37009, 88460, 130101, 7210, 53853, 538, 65120, 151950, 55806, 163748, 52837, 13153, + 21100, 16674, 64536, 6091, 138201, 44837, 58547, 3723, 163, 2177, 32288, 85454, 34033, 8497, 14282, 25742, + 10535, 10741, 79559, 117493, 243787, 49337, 100718, 79495, 40139, 42956, 7551, 55433, 15421, 31509, 23034, 45081, + 547, 61176, 53434, 328001, 8470, 36263, 30145, 4519, 74173, 53935, 11845, 73774, 60211, 78025, 3, 4102, + 73782, 109293, 315332, 48412, 26683, 13714, 6865, 20128, 18490, 104141, 325, 39470, 171970, 115860, 15707, 7268, + 73301, 74336, 31370, 2368, 111827, 107757, 136231, 142844, 97138, 96638, 84053, 38691, 23801, 1588, 10573, 122098, + 77039, 240, 186135, 146101, 11996, 18143, 112963, 46171, 155836, 348769, 47795, 121213, 116266, 132515, 3344, 144804, + 31286, 99187, 255838, 129694, 35894, 48779, 55235, 148582, 71967, 65282, 15174, 13920, 47080, 6147, 108242, 157593, + 125025, 7136, 1286, 28957, 127956, 28402, 98813, 20805, 7532, 109417, 40610, 5041, 32958, 15142, 18408, 108596, + 33543, 50517, 27748, 80114, 233434, 91447, 487, 37094, 100048, 30541, 43477, 10639, 89862, 155868, 37667, 8726, + 60684, 237903, 73408, 99589, 12190, 38739, 97348, 3914, 13594, 2680, 149016, 13907, 30171, 28343, 23530, 115225, + 61104, 35821, 147679, 14337, 4297, 244282, 24085, 326976, 56428, 7851, 21303, 131620, 71446, 83253, 68692, 111870, + 5224, 15813, 38197, 49026, 45057, 13660, 3306, 76345, 40671, 27905, 91072, 996, 68527, 62085, 91351, 122634, + 55109, 168209, 2024, 27560, 112707, 17352, 8306, 167115, 169921, 166958, 5031, 46020, 11844, 67284, 19130, 76185, + 6920, 32849, 5450, 14610, 22451, 21002, 17392, 31872, 66682, 84796, 13709, 40210, 59898, 12029, 8719, 53564, + 21462, 91884, 21647, 88379, 194428, 12754, 37797, 132826, 160016, 22567, 54383, 53186, 77611, 31107, 8339, 4694, + 19185, 90355, 23597, 17222, 140675, 28442, 23668, 55977, 9128, 61555, 28774, 155229, 17658, 9390, 24379, 69357, + 15752, 127381, 239631, 62460, 93181, 55913, 45133, 140155, 18676, 25249, 33164, 29581, 82837, 67223, 22362, 29975, + 7317, 52813, 1943, 29613, 20012, 207130, 49617, 49651, 5636, 15334, 36313, 29226, 28084, 95247, 72072, 19000, + 224932, 15811, 114, 32127, 38097, 37508, 88507, 37225, 27359, 91626, 12193, 69279, 20608, 11055, 88156, 92808, + 2152, 57259, 55275, 72789, 24475, 104414, 1708, 9882, 3818, 48661, 66897, 1631, 34806, 227930, 85815, 87753, + 18321, 250664, 72733, 25107, 206797, 50891, 8082, 196411, 92596, 96764, 152823, 65514, 22819, 387277, 62176, 51225, + 40329, 15563, 189, 3659, 73670, 64357, 51793, 275136, 33482, 86653, 74615, 67058, 11318, 125720, 15388, 22388, + 8267, 1730, 102663, 170910, 40784, 7144, 85373, 13040, 7088, 94309, 583, 44224, 140424, 77439, 18496, 164026, + 36578, 4722, 9151, 5824, 63365, 26510, 35199, 40500, 79277, 32495, 44614, 35233, 9566, 203293, 152144, 7097, + 2330, 183480, 98629, 13423, 330887, 44130, 68600, 30939, 97829, 31012, 345465, 56747, 94879, 4939, 160027, 149761, + 99423, 46099, 32251, 15332, 8761, 96094, 128555, 5763, 235318, 222223, 55729, 30241, 55420, 201746, 3987, 81382, + 8259, 49325, 23287, 7719, 24633, 251100, 92311, 18591, 110533, 64759, 170260, 393860, 7175, 21144, 132887, 3593, + 75346, 101277, 91109, 16387, 259187, 11627, 57459, 173829, 44694, 55780, 49797, 89192, 120443, 62622, 3904, 14814, + 23887, 1027, 112258, 64955, 99800, 11132, 66353, 36202, 48624, 18158, 88481, 96882, 43059, 11040, 2455, 7077, + 21651, 181159, 99126, 100434, 61388, 68186, 19161, 110468, 120052, 8819, 55324, 41494, 7014, 37689, 3618, 87729, + 92615, 207943, 9823, 128657, 12587, 15857, 6379, 67628, 51216, 71775, 157617, 63244, 1503, 3864, 218754, 110864, + 5769, 21492, 7243, 1192, 87921, 85529, 31512, 18537, 42698, 35350, 73510, 84474, 34301, 8991, 21013, 35034, + 566, 38832, 19838, 35586, 37216, 39413, 55006, 12178, 59742, 856, 84563, 6900, 25632, 17437, 49786, 30723, + 13847, 70845, 4044, 7843, 23944, 235976, 55530, 48942, 6518, 20939, 73769, 192653, 52936, 95207, 23895, 132542, + 142982, 22632, 87452, 48042, 54018, 178468, 10728, 26230, 23559, 363, 81269, 142012, 5718, 346258, 31456, 84333, + 246476, 51018, 66692, 101804, 120570, 39962, 30373, 70593, 2864, 60541, 19425, 54209, 104092, 7201, 31545, 48018, + 25865, 15442, 46257, 40443, 8328, 6451, 111782, 47527, 97754, 33046, 470, 245116, 31095, 39, 91934, 87208, + 73470, 36708, 36521, 12801, 70624, 36272, 8892, 79768, 12427, 55454, 103756, 5908, 52390, 62962, 22720, 141138, + 94634, 41689, 128402, 126390, 6628, 106394, 35527, 134394, 82727, 254651, 194502, 148064, 89549, 3202, 28359, 957, + 21954, 27906, 49840, 142747, 8307, 24206, 48978, 1186, 71728, 133038, 71474, 91306, 6333, 110959, 74600, 70387, + 18983, 62609, 56057, 22970, 1147, 135850, 1321, 28834, 3578, 59715, 102227, 32827, 81415, 99952, 55636, 257598, + 390, 22702, 35701, 85872, 402916, 39216, 189795, 14929, 19467, 10112, 144422, 61514, 5279, 63421, 134686, 41436, + 8424, 51925, 10598, 132295, 124416, 4604, 194739, 210929, 57866, 31829, 51626, 50007, 9976, 91878, 61906, 56168, + 81906, 60918, 61859, 40017, 23059, 16887, 40927, 62064, 12785, 32893, 32913, 21782, 93965, 20169, 44387, 79084, + 38463, 11457, 93950, 27127, 157050, 2697, 337088, 5116, 54128, 48255, 33279, 8821, 27352, 25515, 124022, 65710, + 28906, 38557, 33390, 1722, 104435, 72215, 38551, 12094, 30978, 25113, 6671, 37355, 175109, 42862, 98024, 65406, + 221276, 59624, 118012, 64637, 78760, 86697, 21426, 1639, 40350, 12584, 67193, 84144, 31396, 7863, 143011, 69629, + 63112, 9454, 28666, 65798, 46372, 134721, 6314, 51402, 30837, 151922, 2847, 38676, 38008, 92823, 136245, 17540, + 5504, 109295, 205242, 37606, 5211, 214892, 1586, 20670, 208711, 137743, 19328, 40652, 16995, 20023, 14657, 154919, + 34422, 12996, 13918, 38221, 47690, 16398, 2959, 37680, 89122, 6721, 198469, 91876, 172043, 83898, 101992, 26084, + 94570, 3635, 76958, 22853, 76497, 38266, 176590, 168403, 44464, 142840, 79180, 184594, 1984, 41806, 83147, 11985, + 6546, 366068, 59732, 24533, 271505, 8736, 39084, 222992, 93429, 28962, 58985, 86665, 8432, 30028, 14548, 32439, + 54424, 165029, 55175, 27458, 69046, 121277, 46168, 33732, 20661, 24581, 135574, 123110, 37556, 79260, 72611, 16957, + 12939, 46162, 58238, 44907, 72936, 253758, 41324, 32518, 96480, 11949, 124438, 65280, 43256, 34107, 53533, 43531, + 37037, 28366, 45970, 32741, 173438, 6121, 194202, 62969, 26355, 30314, 58370, 28455, 1848, 50519, 82830, 90393, + 21761, 295490, 10936, 256940, 133568, 44050, 20269, 4089, 27457, 21610, 219460, 36743, 14821, 101388, 52005, 13124, + 30979, 140816, 167362, 26054, 18458, 60789, 34917, 40447, 26606, 33422, 9066, 3452, 83614, 5761, 20263, 137238, + 25038, 91310, 101, 52322, 74548, 42572, 38084, 214054, 186568, 31802, 17665, 30620, 141936, 37730, 14420, 4265, + 187218, 49640, 188208, 51441, 55388, 96452, 66659, 40869, 42039, 60967, 221027, 19234, 178581, 29105, 96050, 9165, + 196118, 157335, 3738, 40354, 117436, 2965, 34136, 59659, 15570, 50843, 230035, 31444, 71260, 43886, 18316, 5387, + 38500, 168508, 17406, 32174, 8828, 103373, 143806, 90367, 3560, 18719, 122310, 16508, 26719, 2541, 105429, 6645, + 37998, 73190, 10591, 235916, 49737, 87112, 233941, 53188, 32193, 79154, 4544, 52905, 126477, 7580, 63501, 57314, + 3216, 31337, 6541, 103083, 60846, 49, 9756, 15481, 1355, 43840, 14319, 13743, 27486, 10222, 73114, 230718, + 418644, 16706, 6674, 279748, 23058, 45273, 295831, 86306, 2743, 5535, 88773, 21829, 35253, 120938, 31153, 3169, + 16839, 42847, 8751, 80974, 33942, 36867, 35514, 16485, 26474, 77775, 56877, 5391, 48346, 3882, 108713, 31403, + 27804, 55248, 26235, 43821, 136104, 40118, 175507, 28034, 203908, 18732, 1788, 34030, 106427, 36958, 54359, 7251, + 44936, 15356, 69139, 455, 157915, 22173, 140291, 50348, 43275, 82066, 49621, 54952, 15216, 36226, 96695, 66855, + 6936, 1987, 8227, 196087, 4631, 68827, 99004, 47541, 110265, 17953, 147605, 110242, 58520, 31312, 38724, 329975, + 642, 3155, 34497, 75937, 6207, 73843, 6120, 17249, 51429, 117746, 3218, 910, 68961, 319671, 14938, 29555, + 34700, 1649, 66673, 72268, 9655, 76800, 153087, 6941, 210168, 27130, 35398, 1780, 73242, 3135, 56689, 19556, + 165307, 8765, 35967, 121458, 13333, 70453, 17350, 117253, 22265, 13340, 44265, 39869, 441, 3742, 135025, 23581, + 33309, 16543, 17731, 13291, 157637, 283005, 21408, 101360, 63887, 52312, 83873, 5338, 233779, 23759, 186949, 34531, + 177320, 38069, 156465, 91004, 19353, 59852, 68160, 14891, 1338, 1072, 29823, 1950, 28901, 81407, 313445, 73038, + 84807, 162348, 240257, 37162, 138934, 16111, 58013, 41253, 102951, 16457, 96056, 19541, 56402, 67217, 41638, 94381, + 89674, 29481, 37456, 80815, 151579, 13937, 13683, 132537, 19699, 134545, 67020, 29816, 222341, 141235, 427578, 48868, + 129557, 233342, 23077, 87871, 16213, 18728, 16184, 9469, 37913, 19680, 2798, 171356, 178328, 13216, 50049, 72690, + 71904, 124644, 55455, 7504, 29052, 41036, 266546, 19899, 30391, 188755, 8659, 59469, 16, 104298, 112943, 53865, + 76203, 138226, 68857, 139953, 14125, 107625, 119795, 173133, 4398, 50273, 48808, 54390, 16466, 122086, 31835, 67035, + 50971, 48859, 7508, 46427, 66477, 73021, 84615, 39985, 83076, 46779, 201569, 53336, 36443, 60865, 168164, 143810, + 51393, 25548, 169307, 32896, 24485, 38424, 21837, 29087, 275813, 51674, 6714, 64883, 46169, 187369, 55186, 76192, + 12852, 12018, 62134, 31067, 118303, 16542, 12125, 10579, 4928, 26291, 43854, 7091, 10946, 253716, 109062, 39283, + 17261, 113012, 258512, 47764, 125126, 32646, 55892, 80279, 201623, 149872, 3192, 385, 1208, 48750, 5376, 58738, + 22335, 5427, 82416, 47811, 32435, 143086, 38930, 94128, 59975, 156037, 37977, 38224, 62485, 7698, 50405, 71027, + 16462, 21559, 136153, 34131, 107506, 162069, 63703, 3101, 215029, 40407, 4178, 3774, 9187, 80019, 17880, 97926, + 67579, 2600, 18405, 8351, 47924, 86638, 70820, 92206, 86453, 29610, 42241, 119200, 3198, 15466, 67813, 57863, + 35454, 4779, 99518, 4649, 104641, 144269, 33730, 38073, 65864, 6838, 109456, 193298, 154007, 5623, 45741, 30846, + 182578, 25573, 157224, 1543, 58575, 138703, 146140, 44971, 49356, 18275, 59064, 20300, 13122, 11848, 24453, 11973, + 9797, 86843, 2919, 25530, 49210, 1130, 161220, 76788, 75373, 85604, 34926, 36014, 17777, 17255, 51533, 11676, + 92226, 51845, 119859, 21525, 5936, 18507, 28050, 1140, 31418, 14857, 34207, 47859, 10750, 36382, 32079, 106909, + 59426, 87757, 38393, 110042, 15965, 97104, 33757, 35344, 97993, 53979, 33651, 45407, 41884, 82515, 173089, 7177, + 58371, 35365, 47543, 51927, 35587, 10670, 23544, 29306, 84233, 39976, 76076, 62097, 9007, 8668, 28119, 78281, + 120790, 19835, 143020, 54968, 18670, 64959, 20649, 34469, 42570, 33001, 136570, 87796, 120044, 1106, 58700, 63951, + 127623, 12805, 83057, 40212, 31773, 49850, 7361, 54336, 347524, 101314, 23751, 19569, 48791, 29174, 49369, 20467, + 7465, 75842, 38281, 623, 112457, 60210, 28849, 51003, 94720, 6426, 90047, 85560, 43761, 3579, 85105, 34607, + 90410, 118528, 7224, 42907, 111163, 18168, 6960, 161135, 191298, 5247, 100584, 127552, 171568, 20121, 91173, 12636, + 54615, 20199, 63730, 98105, 2396, 40387, 14438, 125012, 4765, 33235, 12865, 45299, 37728, 82098, 77872, 114037, + 59253, 19675, 24838, 398016, 102561, 11446, 17069, 57508, 178277, 65836, 99941, 26114, 2585, 271882, 136866, 50126, + 11027, 155648, 118367, 14585, 8910, 123015, 335383, 40434, 41016, 53021, 14439, 87098, 176860, 201543, 121888, 2358, + 9286, 5739, 22666, 54270, 37884, 169381, 33984, 93859, 16124, 89364, 72207, 51639, 76366, 99029, 65812, 2198, + 12147, 174891, 194289, 6986, 30252, 88822, 21284, 11445, 288337, 160821, 33034, 100869, 43852, 25761, 52882, 1144, + 103809, 1924, 84458, 86079, 43411, 13542, 139276, 18141, 34978, 41298, 7276, 26481, 173800, 33210, 17951, 142652, + 33616, 33677, 2210, 19941, 98568, 2486, 192414, 80136, 12058, 235883, 50963, 249638, 29572, 27221, 47034, 6124, + 72107, 63346, 97620, 158513, 299699, 40388, 23235, 37176, 224244, 198386, 121323, 67992, 23827, 63170, 17838, 106622, + 158590, 26807, 5345, 23489, 91891, 55474, 74834, 37981, 13058, 5977, 72552, 34706, 26828, 145172, 19904, 21367, + 34043, 960, 77092, 91381, 4733, 47446, 7680, 41697, 5170, 16960, 14741, 46101, 13656, 473, 51842, 37433, + 11103, 11551, 121951, 13191, 97536, 165932, 50397, 51628, 129028, 9069, 44885, 6590, 59195, 47045, 32940, 225472, + 90345, 21833, 13303, 29407, 96615, 141951, 5198, 6028, 18395, 7181, 3861, 14966, 156358, 167182, 36529, 55253, + 25942, 173153, 30959, 27261, 50691, 150176, 162201, 38467, 48462, 80602, 42163, 118482, 168, 108756, 26011, 17166, + 54149, 456538, 22512, 91374, 13816, 90358, 131615, 18132, 226707, 1824, 28139, 26860, 42253, 93877, 77351, 65575, + 8980, 80574, 22020, 27948, 40422, 91324, 76376, 13528, 39281, 91685, 82215, 122541, 144066, 1983, 193851, 17283, + 26320, 2739, 194978, 4790, 26845, 42627, 61300, 65815, 174612, 55133, 4200, 191130, 79771, 158321, 52280, 166796, + 221620, 62461, 11278, 4067, 88152, 83409, 31717, 121367, 13522, 47325, 37945, 10406, 174348, 249321, 154101, 64912, + 29938, 51775, 17220, 15776, 166138, 78890, 84425, 54121, 42861, 16368, 24572, 291647, 10197, 32073, 22651, 11677, + 97509, 26952, 35787, 18424, 41910, 71614, 94977, 72318, 41594, 70024, 275419, 37702, 60199, 7335, 39107, 61315, + 18271, 18394, 33768, 87884, 104277, 123724, 7277, 56288, 71981, 189803, 49320, 3352, 6798, 14240, 8954, 69220, + 94433, 57372, 28620, 68863, 193727, 85575, 42309, 41667, 67689, 42081, 22543, 44824, 12719, 28540, 114236, 101553, + 27638, 27296, 4300, 5353, 4663, 19379, 94098, 3758, 95888, 95144, 80344, 87320, 28447, 259518, 12718, 71391, + 152731, 37063, 24132, 31911, 104896, 15672, 103782, 1521, 4945, 72541, 23717, 122632, 15619, 87175, 206120, 29428, + 189780, 61416, 28350, 44457, 972, 1175, 47233, 198738, 95789, 41907, 21953, 97034, 59341, 22864, 53713, 16873, + 32971, 20693, 20954, 31336, 21477, 16169, 38370, 16412, 9019, 3841, 24599, 21938, 17085, 6484, 81198, 76413, + 5849, 72514, 12320, 65247, 276175, 37234, 59796, 52642, 16312, 57349, 198507, 94148, 46134, 18958, 125552, 1747, + 18725, 151873, 14901, 5490, 68287, 29470, 3689, 64794, 40814, 26018, 25692, 54450, 2703, 88278, 124886, 173087, + 174000, 24159, 179477, 24276, 46004, 201876, 209202, 445, 52876, 31948, 30206, 157610, 39180, 18439, 44124, 50469, + 5774, 96278, 222758, 200216, 50290, 45486, 20435, 46986, 46276, 140133, 142326, 15569, 13363, 47522, 92583, 2182, + 7135, 16853, 22998, 30272, 4952, 63263, 35623, 39096, 53789, 44864, 20053, 110392, 124213, 4630, 16087, 28221, + 127787, 25839, 77481, 44693, 13464, 113146, 6983, 27069, 55717, 50102, 4760, 7107, 26186, 66507, 59145, 36032, + 104182, 71328, 29425, 64317, 50781, 47465, 94298, 69706, 74899, 22754, 120756, 25108, 93077, 56834, 73286, 39928, + 16218, 41699, 176763, 7555, 70819, 50083, 26895, 23315, 26014, 16773, 123079, 41712, 5719, 31516, 90427, 158540, + 85051, 183128, 40864, 27505, 55392, 9058, 45224, 96857, 30901, 136622, 96557, 56304, 120061, 11501, 151448, 5773, + 89743, 7769, 86069, 2935, 18471, 41628, 10114, 33660, 110170, 49479, 26745, 92846, 33221, 26731, 18795, 87076, + 8550, 2100, 29972, 120289, 3077, 72490, 33784, 2630, 208722, 50861, 63483, 79029, 6419, 39467, 14302, 45286, + 64207, 9686, 67513, 44170, 1050, 77246, 59266, 17055, 53801, 7150, 11111, 42432, 4278, 94579, 362117, 36175, + 42902, 41933, 39002, 98489, 22913, 74161, 84773, 57036, 17556, 162288, 74485, 178760, 93867, 73635, 128860, 50362, + 261, 67455, 80001, 46080, 35662, 4368, 25247, 19230, 74393, 22588, 1822, 27682, 235324, 13798, 85998, 13194, + 235067, 23514, 71669, 147632, 23191, 134748, 214683, 105101, 1518, 25489, 247114, 7380, 54842, 26922, 3971, 26361, + 20844, 68642, 170517, 77339, 123255, 8963, 77818, 150998, 48466, 36806, 2732, 23261, 11741, 236162, 18243, 126216, + 28690, 50546, 16385, 92760, 197383, 246558, 201295, 88255, 67588, 71687, 176076, 172653, 169058, 33906, 63747, 24835, + 157621, 43338, 30050, 46152, 132741, 2770, 51371, 94835, 6614, 15112, 11749, 56936, 1250, 19027, 399017, 58036, + 100215, 23388, 55815, 308768, 124152, 94803, 9521, 64186, 8971, 28, 30427, 62163, 7616, 103838, 35079, 29203, + 131235, 7743, 17389, 10882, 37420, 61460, 228512, 85363, 41581, 131077, 62822, 119647, 10130, 54445, 26925, 19968, + 29016, 24446, 74028, 24176, 61448, 67185, 9254, 8563, 119129, 9771, 99184, 37716, 39514, 10532, 221512, 258753, + 218630, 55980, 23394, 32141, 61924, 66749, 32411, 3741, 36475, 26678, 77010, 44946, 91203, 128749, 116953, 20476, + 49625, 53116, 13735, 102335, 29376, 51946, 83407, 67892, 59212, 34685, 21083, 1546, 112982, 32972, 74397, 1078, + 190545, 16082, 86140, 58591, 89611, 101531, 10061, 105104, 76319, 20035, 17551, 52611, 169061, 190842, 100780, 23907, + 90413, 115619, 9675, 34710, 193435, 49443, 129734, 11183, 258877, 16318, 136182, 126808, 44635, 27304, 192375, 2599, + 125648, 47051, 12091, 23814, 721, 58800, 40137, 66726, 97930, 60877, 74487, 7942, 54326, 9841, 41428, 13762, + 8211, 85383, 6950, 99177, 79806, 201786, 296464, 124087, 13144, 29741, 41721, 47634, 55088, 254286, 106408, 17041, + 99064, 12942, 64086, 45233, 14005, 2612, 55827, 255, 7984, 13980, 38574, 12776, 46654, 73499, 249951, 2101, + 26676, 25996, 132326, 116415, 119062, 50449, 31033, 23038, 11589, 179252, 20007, 14860, 129270, 21143, 17796, 144715, + 60106, 70758, 69842, 34674, 282133, 44014, 16774, 57268, 38528, 24053, 46373, 201667, 28327, 471023, 51889, 102667, + 21193, 114909, 84132, 69317, 96723, 67969, 16134, 68145, 15058, 28765, 32035, 2524, 101089, 98664, 25045, 76571, + 14957, 86040, 118506, 262428, 154764, 81573, 39681, 283900, 73287, 127825, 544, 80448, 52347, 38512, 175971, 15180, + 45467, 33086, 46552, 48894, 81107, 43213, 36672, 54025, 76703, 8053, 7608, 13299, 56619, 20752, 238099, 54164, + 105133, 1444, 32942, 953, 37564, 8000, 66316, 119463, 106817, 404, 13667, 149108, 128597, 31267, 10269, 49836, + 106150, 1484, 52330, 76965, 160486, 171648, 38456, 31263, 22424, 37738, 66245, 67467, 143369, 60471, 75610, 20895, + 115528, 86070, 60854, 40796, 49347, 18989, 15030, 11371, 37578, 15779, 79867, 10187, 86462, 46402, 155626, 93200, + 40229, 7090, 57547, 108053, 99598, 11088, 47505, 41218, 206017, 2173, 20988, 30219, 22919, 80563, 57566, 42369, + 93141, 41675, 2407, 182519, 120495, 27154, 16702, 29456, 14349, 7958, 16688, 117177, 140375, 42467, 261919, 74916, + 153569, 10836, 34742, 49526, 7621, 105997, 12212, 2270, 392377, 7755, 17959, 25086, 232152, 138791, 33847, 13860, + 35316, 5811, 1344, 71259, 50452, 207539, 92635, 50359, 5821, 33674, 30255, 2086, 2587, 96264, 17543, 42, + 6029, 9580, 43007, 139248, 82831, 12917, 29607, 25786, 51467, 42137, 85161, 100698, 31561, 88989, 121990, 278500, + 3602, 109344, 37982, 15279, 116442, 28936, 30880, 87894, 58079, 128661, 126731, 67392, 28051, 146885, 4861, 16216, + 97344, 42827, 147561, 153948, 22684, 21335, 47685, 1853, 43349, 15185, 59642, 10229, 25520, 187921, 108972, 5579, + 98037, 24945, 6697, 19193, 63734, 137934, 75056, 89740, 19767, 224268, 56138, 63643, 151661, 39313, 70618, 84031, + 89723, 84074, 13703, 85626, 35460, 8867, 64845, 3439, 57906, 99776, 63968, 49270, 81130, 34356, 16210, 23547, + 36446, 34090, 140028, 72439, 2221, 22163, 57058, 363492, 113754, 18913, 95451, 48663, 54464, 54037, 176097, 68425, + 3023, 34906, 29482, 117389, 341780, 80431, 58330, 16753, 92616, 60907, 94846, 147486, 4498, 48646, 7773, 46801, + 7778, 18946, 464978, 47558, 33223, 177444, 7328, 15626, 63337, 94700, 11743, 9351, 255024, 39098, 16447, 42647, + 96230, 39769, 58840, 10068, 63439, 35800, 65843, 58823, 413844, 9156, 51258, 7434, 61791, 85018, 6872, 3692, + 28096, 7121, 33024, 6009, 75532, 31997, 192535, 9661, 3304, 9547, 14753, 31987, 25314, 55689, 15896, 20430, + 39472, 31340, 99744, 25398, 115569, 54883, 28719, 205423, 23071, 57855, 64638, 149867, 25671, 82403, 37616, 20668, + 39989, 77996, 74948, 140555, 175248, 64810, 36515, 46595, 4958, 248773, 24045, 28728, 136673, 168704, 20804, 114833, + 100325, 27135, 21205, 96151, 153134, 45992, 7093, 13992, 76047, 1980, 19432, 145001, 75159, 87462, 17710, 1013, + 45556, 34297, 144882, 20648, 26061, 11319, 129567, 108555, 18872, 464580, 33386, 22717, 65948, 167189, 5603, 135042, + 79542, 8801, 202632, 18114, 91882, 5973, 5239, 67315, 4431, 60916, 47819, 71693, 32597, 32606, 18183, 45072, + 80329, 76385, 24749, 51305, 40314, 156514, 14693, 130345, 13168, 66214, 18029, 12858, 34801, 27628, 14544, 10823, + 40522, 40185, 33739, 148694, 23548, 9923, 61012, 28859, 17933, 19442, 34364, 99849, 164107, 141167, 30629, 21054, + 6744, 36491, 8096, 42474, 41706, 155060, 30650, 10600, 163442, 1143, 96655, 61390, 52359, 7559, 51568, 64256, + 203854, 4467, 22453, 14504, 436398, 7878, 6980, 8293, 63610, 293747, 16167, 35763, 19627, 147603, 15419, 18032, + 110744, 51346, 33681, 54571, 40472, 48615, 39073, 21604, 13754, 173027, 92560, 11083, 47299, 63062, 11813, 52007, + 29883, 9734, 139722, 15953, 1550, 20651, 13616, 49306, 16113, 90089, 92326, 7584, 30712, 72424, 164858, 6831, + 152871, 55746, 197721, 34167, 196442, 6022, 112107, 55215, 7538, 123381, 4920, 43539, 77165, 8939, 50392, 34192, + 20225, 79762, 22505, 58667, 40770, 29788, 97180, 82835, 4568, 8579, 13273, 363569, 35898, 49983, 436, 36598, + 3237, 131691, 62418, 35591, 8101, 4073, 379438, 65218, 76072, 33887, 2968, 27573, 212619, 288680, 68278, 72851, + 150504, 217896, 6913, 121339, 22017, 35340, 51072, 43616, 75043, 31437, 10833, 81487, 4364, 22968, 41454, 106687, + 85446, 19863, 109625, 149241, 524, 141850, 214404, 54376, 657, 237023, 9401, 108137, 53800, 32474, 49712, 53334, + 126876, 27337, 45552, 177696, 8269, 15036, 12097, 42240, 2328, 125374, 119295, 99715, 2500, 19624, 39441, 27220, + 102691, 60957, 94543, 39101, 18566, 67362, 13975, 78230, 25017, 34017, 239007, 90027, 39351, 41681, 35354, 43822, + 1043, 916, 58587, 141983, 94818, 38799, 75459, 41114, 67432, 16195, 36606, 59568, 22272, 126769, 31424, 68659, + 12287, 134302, 257977, 5756, 207285, 95637, 47248, 117689, 19583, 77451, 22373, 12200, 54993, 117118, 34244, 29386, + 34562, 53819, 71267, 64172, 77665, 49368, 7716, 59301, 25749, 45426, 194789, 17297, 2650, 1766, 32501, 45198, + 20403, 20984, 6600, 14171, 94604, 19037, 5402, 29896, 9938, 59935, 109708, 88081, 145182, 44844, 39167, 352626, + 164173, 35374, 45982, 6122, 154, 73419, 220487, 53834, 53601, 17992, 8609, 229321, 5610, 68098, 66815, 71012, + 95069, 140968, 27396, 8957, 134489, 24656, 86659, 56598, 134852, 17316, 123838, 255436, 6613, 41610, 138033, 81452, + 32023, 32396, 123687, 63398, 8693, 29712, 30407, 19296, 121188, 3551, 36099, 20032, 111948, 56624, 16547, 27453, + 35916, 15378, 52039, 56849, 13489, 22214, 73177, 53097, 277349, 2157, 14029, 187886, 10260, 141743, 246460, 91880, + 50869, 3788, 49486, 133566, 54950, 33120, 129337, 53768, 18333, 9525, 26902, 312251, 10297, 9020, 70759, 16647, + 112432, 59260, 84609, 9818, 82766, 73569, 468, 46001, 75780, 55028, 52106, 11498, 43645, 108069, 17150, 17753, + 29417, 16705, 31799, 9606, 289, 122254, 115975, 8620, 6133, 255357, 56908, 14456, 133464, 43554, 79224, 11247, + 29630, 160, 12756, 25464, 65960, 350428, 62521, 321796, 100359, 67358, 35169, 46172, 113128, 48988, 88868, 31094, + 33266, 6847, 60887, 98188, 49659, 69117, 92977, 220228, 13947, 80181, 35103, 62170, 97351, 13475, 2440, 199768, + 19498, 36597, 46971, 25234, 67806, 62881, 84717, 73648, 181966, 10488, 94149, 21550, 26655, 63436, 48375, 14405, + 165650, 9621, 24439, 28043, 42735, 4490, 29963, 56674, 45373, 1934, 262446, 50855, 67098, 26898, 5261, 52696, + 40644, 33900, 9440, 180286, 87162, 22940, 19704, 26936, 69769, 10254, 101759, 27406, 12243, 48000, 73926, 113215, + 54935, 5726, 192787, 4312, 106216, 9366, 11550, 52949, 23457, 212271, 277152, 133895, 108374, 6191, 96477, 29980, + 218916, 58024, 54696, 40853, 91124, 65894, 91170, 65908, 252552, 6793, 29212, 15389, 44516, 122515, 52617, 35058, + 9017, 103536, 39510, 49136, 19242, 130652, 662077, 74699, 47024, 31422, 8517, 73351, 24399, 13867, 128360, 4810, + 4434, 61779, 111983, 61036, 17798, 110240, 59722, 102960, 39688, 10001, 23803, 23039, 176498, 56659, 44814, 134295, + 17188, 77577, 74466, 226175, 102472, 154333, 63900, 111747, 18062, 41171, 79669, 32773, 408933, 42562, 28931, 30907, + 107388, 43487, 2946, 240310, 23938, 24354, 319, 184983, 7927, 6488, 1422, 10790, 68809, 68209, 64775, 4361, + 202, 17123, 59634, 51200, 44391, 18188, 17843, 2619, 74278, 3230, 9540, 47187, 21702, 36274, 56894, 43907, + 16310, 34790, 16866, 6150, 5561, 13587, 107545, 108873, 126867, 86986, 28640, 33427, 19017, 5762, 80637, 17430, + 46903, 2047, 131055, 25958, 13558, 5444, 47152, 13900, 44563, 122857, 45348, 70863, 39593, 54332, 38068, 33637, + 318, 40310, 143467, 18502, 24520, 11377, 62013, 28942, 27246, 28269, 83545, 17999, 59015, 90707, 30065, 15161, + 34720, 1263, 37008, 2012, 6060, 98575, 92933, 5721, 299, 199555, 24578, 29223, 2985, 743, 115825, 109523, + 136657, 47454, 26378, 53586, 3733, 174945, 93340, 244456, 5693, 37386, 28782, 89767, 27545, 23573, 18798, 136425, + 34320, 84778, 20041, 48453, 38215, 7477, 71958, 40621, 8773, 5874, 187927, 105965, 51100, 43533, 18083, 8443, + 10180, 43597, 2003, 183999, 69689, 12216, 129696, 146188, 62389, 34044, 68410, 12765, 43273, 26949, 266807, 3345, + 34477, 79197, 5688, 47539, 213110, 21634, 22257, 50092, 32222, 42346, 39530, 63668, 98, 134978, 74022, 5152, + 59088, 174145, 37220, 9934, 9545, 118937, 5724, 87240, 19875, 15784, 40143, 23263, 87513, 181654, 285152, 37881, + 263241, 4966, 43934, 10433, 186657, 6470, 74416, 225854, 25908, 142677, 246262, 32280, 6192, 75890, 45546, 143264, + 135305, 29742, 47013, 77787, 11732, 126658, 8763, 37950, 21806, 57557, 113464, 89465, 108995, 164574, 23894, 22996, + 23169, 15369, 23117, 17642, 130607, 40503, 36239, 280990, 44666, 9981, 40427, 147487, 26869, 168452, 32886, 32991, + 46798, 240839, 15111, 70502, 65697, 88548, 44145, 28701, 48767, 31139, 206777, 35659, 181164, 166262, 14554, 171445, + 31786, 66523, 76607, 17956, 6507, 31279, 90476, 116611, 167918, 6560, 1243, 115324, 80128, 41867, 55897, 187323, + 37069, 32596, 189444, 145931, 13390, 105530, 65709, 26805, 6999, 55714, 41300, 22915, 68951, 22138, 21120, 22264, + 10058, 19945, 33635, 56123, 99085, 10032, 5818, 6016, 46649, 57476, 35264, 94413, 112522, 262288, 93686, 83038, + 14341, 23204, 28807, 66084, 77987, 6101, 126673, 7133, 38126, 5923, 122091, 170240, 97772, 46874, 215746, 43948, + 41622, 3272, 55596, 8332, 146411, 251315, 13533, 8561, 81521, 115449, 48616, 175175, 2063, 186556, 3036, 134537, + 75772, 29728, 82360, 22973, 186559, 86348, 89100, 38388, 82297, 45610, 2613, 87082, 9986, 177812, 57884, 23591, + 47485, 42543, 33582, 44713, 74439, 257444, 252451, 31825, 35631, 38540, 33066, 5147, 13973, 4343, 51830, 70378, + 22827, 26448, 95560, 36896, 241741, 48067, 203953, 298860, 61620, 20450, 3220, 67272, 6586, 107662, 100160, 108684, + 6929, 57226, 4762, 7457, 1320, 40404, 77204, 99309, 62750, 208653, 59977, 44000, 74315, 34332, 5819, 172217, + 64904, 114077, 18147, 84012, 1791, 98456, 90930, 21446, 116669, 103938, 7422, 85140, 59713, 5768, 326211, 16239, + 75411, 13229, 29398, 10758, 236107, 1539, 112472, 95979, 152154, 151294, 306, 21196, 38146, 10700, 6891, 84282, + 109646, 56492, 40539, 6589, 119491, 51354, 30685, 140209, 136906, 29622, 73617, 49553, 70525, 51671, 166869, 139616, + 74395, 37439, 49595, 45678, 11959, 33211, 86560, 52434, 9282, 62690, 112155, 130810, 5243, 108261, 99970, 265613, + 72551, 80049, 6391, 33365, 90721, 66737, 69872, 87011, 1860, 9032, 112544, 60905, 37371, 89015, 140351, 19076, + 850, 373531, 2802, 36725, 218795, 72062, 28990, 16550, 24614, 7815, 6187, 26336, 33373, 32162, 42791, 73555, + 32062, 23386, 10244, 56392, 49442, 27076, 136262, 12412, 14883, 1134, 33675, 97153, 199281, 15608, 100152, 74072, + 47942, 254301, 36451, 16026, 10687, 65067, 56708, 254030, 30290, 50490, 13864, 57941, 259331, 35588, 23485, 43486, + 24869, 21620, 92971, 22072, 88645, 1048, 182050, 13343, 32452, 14825, 19509, 3325, 216938, 45740, 99716, 189082, + 53740, 78245, 25609, 24311, 176777, 47340, 308354, 40669, 66085, 14102, 125339, 9225, 128709, 97207, 1271, 200933, + 78439, 113451, 88975, 18324, 46521, 11819, 18570, 141756, 72512, 170020, 52754, 63550, 118515, 103073, 93330, 32736, + 50499, 14722, 31600, 68452, 398867, 29316, 172786, 18417, 104924, 2606, 5670, 84818, 16288, 67106, 59580, 82929, + 607401, 291, 85829, 359, 15897, 35830, 50696, 65630, 52672, 22115, 356968, 29895, 40837, 231192, 34024, 38957, + 26722, 406, 23335, 124952, 72068, 68804, 13268, 147101, 164740, 276569, 162596, 66943, 11569, 26654, 66358, 4777, + 23229, 102127, 5848, 978, 2921, 59666, 5371, 28212, 90108, 42938, 39320, 2499, 4271, 108792, 33510, 125072, + 71653, 65239, 38250, 66357, 38577, 13964, 86251, 35708, 50755, 36010, 29448, 12209, 3844, 38222, 206337, 100876, + 67827, 137088, 14167, 252225, 84163, 195270, 1306, 5703, 54198, 779, 46802, 22028, 51124, 86759, 70560, 113164, + 35685, 162145, 45471, 34561, 422, 2611, 6464, 47486, 19223, 38246, 9191, 18331, 89942, 243642, 212364, 15893, + 17518, 22617, 6409, 30046, 126182, 59716, 36560, 104428, 18846, 26592, 19458, 50793, 147333, 30826, 1388, 27647, + 10922, 14495, 33545, 19269, 135828, 39727, 41601, 46931, 233379, 49169, 131130, 182112, 16276, 82381, 118209, 142445, + 128310, 19672, 28740, 82907, 33436, 3118, 102206, 28723, 24819, 41937, 38854, 5157, 3881, 111491, 1142, 9776, + 421673, 152241, 29309, 14961, 87854, 6054, 15424, 3796, 82656, 54996, 2108, 55367, 239450, 154525, 9643, 118103, + 106041, 64601, 68549, 48707, 30266, 25772, 18740, 9462, 229669, 91798, 112152, 191327, 14493, 72828, 8175, 66636, + 236474, 25817, 87351, 129027, 76653, 20422, 22983, 71240, 27846, 44661, 12399, 46158, 77704, 53101, 35032, 11072, + 17300, 109294, 33638, 24408, 1895, 11241, 760, 17584, 82479, 125877, 63150, 141075, 34259, 23274, 81698, 15732, + 43577, 48340, 91584, 14688, 16379, 24481, 150280, 96420, 262050, 48635, 43727, 61819, 56268, 72003, 88178, 17281, + 79912, 13218, 122519, 125295, 166396, 11811, 2171, 118930, 67746, 17636, 178278, 174656, 95661, 173039, 83845, 79689, + 17473, 98555, 127696, 203415, 54730, 22925, 232239, 9309, 12136, 175026, 20740, 180188, 10747, 39816, 314017, 266131, + 10040, 175732, 112550, 220651, 31974, 37393, 888, 23008, 86799, 4303, 64905, 148467, 75337, 251, 3284, 370102, + 50264, 9835, 5438, 23655, 4481, 29851, 329, 12855, 7162, 64931, 78141, 12804, 42372, 296771, 83547, 18624, + 34874, 86271, 3360, 48665, 77735, 88767, 11463, 63527, 28889, 22258, 29140, 194315, 113924, 25499, 6406, 31334, + 1845, 4802, 49184, 43455, 35469, 127594, 92970, 61038, 115005, 38840, 87761, 106838, 8811, 20572, 55637, 11162, + 96721, 132425, 108925, 2948, 125457, 36356, 3502, 75270, 27622, 127192, 2561, 123095, 49394, 61155, 16897, 110064, + 9699, 89448, 53356, 19628, 220310, 21622, 83036, 9885, 112214, 6087, 26713, 17901, 161912, 91492, 3440, 68594, + 9266, 92238, 8087, 6866, 150194, 72175, 80701, 13459, 31836, 43243, 239700, 95846, 44749, 50647, 21945, 230538, + 120612, 132371, 244604, 5193, 105637, 34661, 41341, 68775, 85393, 1874, 8771, 33718, 49672, 77403, 595452, 99507, + 6490, 58895, 128742, 7704, 39239, 73217, 43816, 62824, 37804, 199976, 22361, 80005, 87514, 94832, 14089, 4574, + 139975, 59142, 75523, 100268, 43906, 53442, 15152, 2547, 186002, 17011, 19513, 204282, 3343, 60568, 128318, 119250, + 4298, 51871, 41336, 71759, 21921, 45074, 98169, 145889, 99427, 11350, 1237, 5520, 28799, 7803, 53702, 21026, + 136352, 38293, 128690, 12158, 90132, 44600, 10184, 26957, 39459, 126025, 78904, 82999, 59373, 39301, 150198, 120529, + 153042, 20177, 50089, 14764, 271571, 30530, 123161, 38975, 101562, 22941, 5648, 124654, 109243, 69817, 71675, 49162, + 106884, 21241, 107795, 30258, 16572, 188262, 141456, 7688, 60718, 8271, 11044, 32440, 104608, 103419, 236109, 93156, + 43293, 128929, 42107, 67180, 25201, 115254, 185488, 130954, 72813, 167547, 20537, 39969, 38432, 22582, 184022, 1139, + 27199, 5655, 17767, 97412, 122606, 209377, 27070, 35871, 326617, 188954, 42680, 73512, 80911, 22629, 3011, 95021, + 315242, 157737, 383, 41821, 41808, 19335, 27950, 15674, 25677, 110950, 35375, 76835, 59108, 57370, 35262, 16569, + 160415, 37706, 78086, 32041, 49691, 137143, 9782, 172080, 50148, 77917, 6323, 10110, 69172, 17711, 21795, 59511, + 76184, 135114, 31046, 132319, 59105, 157578, 20549, 80778, 57649, 158421, 65143, 4575, 72235, 21899, 10797, 92745, + 34035, 106079, 80159, 4508, 78304, 25350, 75457, 46458, 32937, 25623, 47, 8531, 104751, 84953, 8138, 36508, + 187199, 66310, 115274, 13253, 32461, 38536, 1916, 42007, 187160, 35055, 26325, 84394, 35963, 94216, 45590, 97782 +] diff --git a/packages/kad-dht/src/routing-table/generated-prefix-list.ts b/packages/kad-dht/src/routing-table/generated-prefix-list.ts new file mode 100644 index 0000000000..14b0ce00ce --- /dev/null +++ b/packages/kad-dht/src/routing-table/generated-prefix-list.ts @@ -0,0 +1,4098 @@ +export default [ + 77591, 94053, 60620, 45849, 22417, 13238, 102507, 179931, 43971, 15812, 24466, 64694, 28421, 80794, 13447, 118511, + 740, 6439, 164565, 160996, 29829, 65024, 115728, 46297, 71467, 26874, 47057, 19864, 228973, 57886, 62422, 50382, + 196661, 98858, 8131, 154708, 78537, 104511, 53134, 136579, 27689, 126238, 28199, 3679, 36431, 48892, 2655, 57939, + 44415, 38209, 7970, 34780, 14362, 51843, 23108, 52670, 19456, 36805, 408716, 129012, 106025, 12683, 780, 36702, + 96308, 73261, 165714, 94326, 2882, 15786, 65607, 80947, 49509, 13763, 104712, 13107, 21149, 137011, 223495, 30903, + 87173, 75141, 2533, 121964, 131409, 110026, 108394, 16009, 75844, 196819, 1440, 7629, 23676, 111231, 127712, 61087, + 121838, 51872, 29103, 7233, 30291, 24088, 110490, 92353, 17492, 113372, 16487, 97612, 2953, 9394, 210912, 8964, + 7564, 3852, 97455, 42207, 110620, 22643, 65016, 7253, 129477, 46969, 7830, 43238, 127283, 37807, 65596, 47230, + 53113, 68778, 42174, 3025, 72417, 113389, 61485, 3233, 165166, 23272, 207684, 1480, 109690, 77717, 146330, 35614, + 21200, 125839, 9167, 183529, 102125, 27762, 21718, 34784, 24049, 54922, 44135, 54112, 71504, 58952, 18652, 36112, + 90342, 97581, 105898, 116695, 25307, 71711, 19850, 443067, 72039, 164371, 99358, 141908, 26812, 37120, 222981, 92235, + 26715, 2272, 38699, 277092, 32264, 2507, 11509, 41396, 133800, 81066, 75726, 51643, 71161, 32364, 125073, 195906, + 88956, 8820, 58708, 60150, 171987, 43866, 50300, 27077, 51779, 41724, 18910, 42608, 24425, 59574, 40645, 30367, + 16671, 106324, 56018, 73410, 30251, 125091, 17154, 23172, 186294, 741, 111661, 148919, 247761, 71695, 148683, 76545, + 14202, 32826, 57291, 56464, 2121, 52187, 36887, 19845, 8465, 15701, 42227, 10603, 35024, 129005, 20364, 271992, + 4876, 54659, 43090, 48318, 85917, 40506, 60228, 35848, 169730, 2400, 19908, 21535, 3638, 2880, 105194, 37121, + 256836, 27972, 59367, 47659, 96184, 20378, 6352, 132486, 943, 210847, 347244, 42708, 18678, 161556, 4520, 63681, + 6583, 138160, 207565, 4182, 52907, 72891, 36505, 33320, 35807, 152018, 13288, 904, 112254, 139219, 23049, 24474, + 214097, 14830, 47960, 50966, 18796, 25821, 749, 61464, 11595, 123216, 5285, 37544, 9243, 80395, 22070, 63873, + 23554, 106570, 90364, 35779, 887, 61552, 55147, 3791, 268203, 76040, 13872, 53070, 382004, 149091, 9411, 70938, + 24590, 26314, 23297, 60821, 111335, 56198, 123964, 28317, 11625, 39656, 33077, 122186, 16619, 2762, 8556, 43622, + 29039, 54719, 141778, 30583, 102425, 30319, 55618, 4660, 69006, 75066, 46293, 24767, 97976, 8387, 5680, 68535, + 92362, 327684, 180600, 43548, 32552, 905, 167743, 10812, 63717, 48600, 4157, 19832, 41433, 44366, 169717, 362623, + 128974, 242972, 74944, 25914, 137630, 138732, 9905, 65119, 59943, 13001, 10439, 346877, 10019, 72338, 47424, 90540, + 13986, 32605, 74311, 36273, 35430, 43274, 490600, 15654, 33665, 40911, 16891, 132492, 108037, 118859, 30430, 45629, + 43799, 65831, 25824, 63966, 43280, 70552, 34778, 102075, 38195, 5993, 20515, 11742, 29078, 67047, 980, 30234, + 58629, 68076, 5792, 59696, 18265, 2627, 47407, 29302, 14425, 46647, 15604, 15925, 46832, 5440, 684, 42003, + 235538, 28764, 54452, 25101, 40830, 8023, 6501, 50689, 77881, 5650, 16800, 16147, 110717, 28112, 219637, 1634, + 58937, 32412, 88801, 6927, 3463, 157022, 94779, 442571, 325358, 276, 141280, 75559, 51300, 58421, 109559, 35845, + 47623, 321870, 24845, 42379, 117252, 19971, 14000, 130543, 19007, 191657, 1705, 32933, 10170, 64831, 2632, 89911, + 20540, 14737, 53476, 30106, 91237, 23474, 41156, 76048, 294813, 109786, 153316, 31289, 4951, 134188, 5698, 58898, + 79841, 8216, 13373, 150001, 56232, 83956, 179514, 40785, 36270, 150581, 38142, 36729, 128547, 27488, 48397, 32074, + 69209, 83991, 69639, 44375, 66275, 50325, 46119, 4588, 100156, 57453, 106674, 3707, 32063, 12250, 176480, 94462, + 73531, 42286, 44132, 42292, 34439, 205098, 23362, 170867, 80937, 18578, 35224, 8003, 28892, 73415, 50905, 36012, + 44466, 3377, 68122, 77350, 88595, 16048, 139321, 45304, 216307, 26958, 49160, 2333, 32583, 197092, 51650, 27957, + 49620, 28596, 32484, 40154, 16605, 3672, 19287, 14394, 82127, 113881, 101822, 55495, 45807, 22719, 49287, 17105, + 21630, 9213, 225560, 184754, 78726, 55879, 1187, 55736, 20235, 48276, 60072, 8055, 40163, 71435, 10613, 66014, + 111007, 30011, 11754, 32797, 96926, 8244, 35114, 58420, 5567, 8879, 4349, 36989, 72083, 27721, 80502, 31714, + 21665, 68483, 67000, 32243, 58844, 22490, 151524, 85501, 39419, 31544, 46585, 60252, 179767, 135313, 38991, 99008, + 48328, 21411, 230904, 25457, 42662, 73162, 35923, 104338, 51550, 37715, 30664, 24386, 5251, 34179, 21686, 23914, + 37811, 77986, 123822, 22186, 49608, 218194, 113768, 119158, 81056, 136532, 36573, 4335, 50854, 77454, 36591, 786, + 55513, 89905, 64981, 78223, 20922, 90512, 58000, 187805, 18891, 142810, 7204, 125174, 197409, 232663, 64781, 31572, + 164656, 137833, 103498, 55315, 32593, 91963, 91694, 30505, 71449, 150025, 16975, 134836, 220474, 56258, 1789, 23900, + 58919, 39771, 52833, 15954, 85682, 182360, 82050, 60999, 67854, 36289, 50792, 14607, 13758, 73909, 111848, 63880, + 35066, 107613, 145156, 26237, 3565, 8173, 214338, 1836, 61905, 82544, 35483, 19741, 214793, 18510, 3395, 10924, + 119572, 75264, 17466, 43207, 141419, 82668, 39303, 19609, 21504, 19695, 19065, 6944, 10302, 38666, 102996, 88789, + 27354, 75138, 70106, 135106, 67003, 20045, 60619, 54525, 46131, 115306, 12445, 86777, 32668, 68413, 32737, 64388, + 15165, 34095, 171569, 11093, 64871, 119058, 92294, 117952, 34450, 66009, 203796, 6258, 17821, 52488, 314552, 125812, + 2757, 95795, 15139, 46369, 11452, 76801, 3035, 9101, 34189, 14945, 7202, 149174, 5160, 74854, 169046, 30085, + 12257, 76562, 92934, 170882, 85523, 121128, 60225, 45744, 560, 62173, 205019, 128933, 53385, 94, 81804, 5962, + 65887, 9406, 75139, 46078, 119549, 87470, 126330, 115083, 135620, 90768, 93971, 66716, 312353, 69610, 203240, 65196, + 115979, 13452, 77397, 23, 122356, 131305, 48028, 43698, 10867, 95182, 47337, 60657, 193231, 4430, 32675, 100177, + 124537, 49701, 68459, 417255, 54783, 44031, 66481, 29365, 90675, 20969, 21022, 49332, 120791, 87739, 113524, 8715, + 4715, 33049, 64432, 86239, 142253, 763, 145381, 11942, 50943, 44118, 117335, 69368, 17271, 82615, 97767, 8516, + 43358, 61812, 117693, 77645, 25331, 71884, 62816, 56740, 4917, 126017, 38232, 39911, 120566, 45088, 86073, 19308, + 34580, 62715, 98835, 12238, 12878, 32818, 80514, 190672, 33786, 124897, 32390, 13707, 160528, 8239, 24113, 94911, + 32523, 8473, 305619, 143741, 4869, 226676, 116030, 72714, 301307, 245805, 49902, 13070, 104817, 63744, 25320, 14079, + 81491, 66562, 24649, 6335, 23276, 12633, 45891, 31344, 8832, 19031, 49267, 95191, 97911, 27244, 61726, 53839, + 31265, 81626, 4566, 137532, 52065, 115327, 11846, 252068, 7998, 22402, 10126, 209408, 49622, 16068, 12953, 24383, + 9715, 82577, 95468, 95106, 43998, 60754, 21093, 14837, 34091, 72540, 179063, 7433, 84587, 192802, 47914, 4438, + 20664, 45500, 8855, 16934, 69041, 12731, 29041, 217180, 29419, 22657, 137482, 2887, 53205, 550, 70043, 123839, + 10838, 164726, 42397, 184876, 58288, 26641, 22447, 12131, 116145, 22995, 97093, 108266, 6185, 2832, 52427, 64656, + 5154, 49928, 144137, 12044, 141795, 129976, 31641, 84599, 35924, 2502, 28404, 26000, 21307, 63600, 20886, 165871, + 144738, 353334, 45550, 4235, 43730, 54853, 149395, 14340, 12085, 6025, 82291, 127186, 8279, 7961, 81927, 74078, + 10002, 50016, 8795, 38560, 119, 45637, 190798, 21574, 133779, 97318, 19903, 27528, 199668, 1330, 66035, 21635, + 72938, 31184, 60710, 108060, 31768, 145285, 89744, 113430, 39176, 71121, 10578, 19002, 67875, 39253, 95870, 17637, + 38453, 35956, 214432, 92498, 9700, 51981, 75487, 140364, 44144, 248414, 34793, 35244, 4121, 13131, 29680, 132109, + 116048, 51552, 20482, 69742, 41733, 134398, 163626, 2676, 12868, 9786, 36799, 26675, 82669, 19252, 28098, 76936, + 92308, 127797, 49202, 5337, 128, 27975, 178978, 22753, 34262, 94544, 214584, 43276, 11332, 665, 58732, 8484, + 7712, 180682, 90181, 28567, 90764, 20944, 68372, 62049, 36141, 29920, 115786, 1365, 13553, 110638, 163556, 207080, + 71312, 250718, 214174, 18727, 77470, 23807, 32279, 108909, 117314, 4887, 61022, 41180, 96549, 116044, 1081, 78818, + 49135, 8305, 20213, 10021, 23602, 148923, 39033, 76575, 54468, 41625, 121743, 61361, 28605, 110339, 97381, 108784, + 6327, 58565, 37906, 2722, 62308, 42415, 120829, 226683, 17171, 16955, 32278, 42441, 67531, 82112, 7044, 8333, + 21319, 4625, 67693, 83024, 14105, 107392, 18658, 14247, 894, 35117, 78964, 71644, 107722, 11889, 4981, 16504, + 46157, 86476, 243104, 110164, 8503, 65279, 38377, 50730, 51069, 170106, 155778, 36441, 100472, 8367, 14072, 2456, + 45138, 1449, 85419, 56978, 15246, 51849, 58602, 75312, 14577, 34388, 14985, 214746, 35609, 94173, 205371, 29378, + 191464, 60659, 83825, 4266, 1757, 79901, 4005, 96090, 13364, 26836, 20634, 9902, 161349, 52221, 57608, 45087, + 32067, 12041, 24449, 122590, 91705, 4841, 5595, 1962, 81144, 94514, 7189, 65466, 52339, 115937, 30039, 184359, + 5408, 37938, 13094, 131687, 91066, 50656, 3538, 308588, 21983, 117880, 124083, 8740, 14157, 207581, 132848, 24615, + 100545, 35998, 13259, 94379, 4372, 221513, 9160, 14015, 26630, 42025, 87194, 4685, 129112, 37014, 5514, 1659, + 1423, 35031, 86869, 42243, 29676, 77384, 91770, 8949, 213626, 219087, 14943, 2758, 4397, 146113, 19935, 39810, + 88436, 21548, 15622, 47174, 99190, 170858, 31675, 22540, 6877, 25282, 66955, 39440, 49958, 3702, 59942, 3443, + 26122, 118447, 24469, 28429, 114348, 66350, 72579, 194, 60661, 14964, 70751, 30122, 29818, 134851, 14530, 25859, + 293118, 32210, 11158, 134437, 50042, 50868, 124554, 56791, 179738, 112687, 67437, 80580, 16400, 32499, 35433, 38147, + 163423, 62209, 109887, 21489, 89627, 8619, 37255, 42560, 31040, 3283, 221255, 26057, 43973, 176482, 84209, 74565, + 36638, 128029, 50150, 53376, 45952, 23372, 136030, 19408, 5153, 189398, 9461, 12142, 1894, 150004, 6947, 43095, + 109322, 74270, 235743, 8877, 1898, 12589, 62161, 150831, 134021, 76036, 32418, 114411, 12402, 9784, 152424, 2030, + 112077, 39948, 15299, 91532, 68309, 58254, 74157, 68071, 190269, 1807, 48227, 14614, 69866, 175786, 53526, 77245, + 31938, 86410, 49785, 5548, 107383, 26754, 6925, 99713, 11522, 112823, 36879, 191627, 105232, 112178, 9544, 115058, + 11248, 121092, 115523, 216088, 14868, 164602, 6984, 12211, 39852, 3557, 11388, 124397, 71707, 42768, 81029, 87167, + 186525, 134029, 24303, 29049, 16530, 60454, 1801, 70482, 38162, 186140, 17626, 75869, 106212, 3301, 149347, 83560, + 11700, 132692, 2213, 6118, 5130, 19621, 133100, 5413, 16608, 6316, 6903, 20826, 26998, 46988, 14742, 36801, + 59586, 438, 115651, 12542, 108399, 50888, 73600, 74851, 230033, 11883, 313836, 13563, 43683, 27664, 16986, 54266, + 48135, 20496, 78612, 90668, 82179, 65157, 159306, 244506, 2073, 113828, 34210, 8905, 5015, 124130, 30133, 30478, + 196684, 40526, 10545, 25933, 189293, 20827, 73483, 91579, 16378, 24561, 168921, 100351, 23452, 105211, 31749, 3947, + 8301, 235867, 175604, 4648, 35640, 22045, 10909, 12114, 11632, 81578, 50578, 17722, 214551, 40781, 131060, 242797, + 29240, 41868, 116245, 182350, 57644, 27787, 59645, 42511, 33137, 64292, 86072, 2870, 91949, 108278, 14903, 186497, + 55157, 48398, 10332, 2801, 52384, 20759, 10283, 88468, 117313, 23727, 138084, 65635, 5090, 14195, 126767, 300, + 17717, 38157, 16186, 114320, 89668, 96676, 9742, 203368, 49363, 5035, 28964, 65388, 82238, 67525, 39995, 13922, + 241035, 69735, 11154, 193950, 66216, 72997, 12434, 16882, 29066, 91839, 31743, 96167, 184088, 75620, 1030, 139617, + 97206, 15695, 244555, 101352, 62820, 44153, 114812, 120196, 26595, 72217, 5935, 28488, 4241, 7832, 101557, 27041, + 135635, 10308, 337586, 23855, 173672, 15924, 5051, 10103, 8202, 360, 45227, 30801, 459, 13982, 27256, 9104, + 71355, 53611, 81898, 79904, 146294, 57705, 99956, 35919, 29587, 21273, 89804, 41886, 3008, 100905, 29691, 22814, + 135385, 101754, 7790, 16486, 141203, 186158, 135150, 17125, 14803, 43200, 23042, 70352, 6634, 27432, 14596, 27017, + 45094, 251700, 107172, 92556, 69362, 224587, 20275, 239867, 50925, 67860, 22054, 35132, 546, 107574, 11246, 15583, + 51884, 52526, 41469, 90704, 62011, 30436, 4192, 20677, 83296, 40746, 43027, 18829, 234584, 59250, 10989, 12045, + 44515, 87149, 5814, 22428, 56050, 1304, 54193, 102712, 89476, 74967, 28363, 182054, 87751, 63858, 4667, 36435, + 19373, 13180, 80439, 20298, 12691, 59200, 175067, 68478, 149923, 65774, 50785, 75599, 19794, 24659, 40763, 18905, + 13833, 221290, 11814, 27472, 35846, 256569, 9769, 37905, 87557, 16393, 61774, 29056, 58339, 67859, 122835, 31673, + 2884, 29565, 225212, 50663, 19145, 154284, 7940, 13382, 25647, 46917, 107024, 18714, 12224, 8197, 11896, 129114, + 11024, 5323, 163976, 216168, 77338, 91508, 61901, 29134, 64608, 87645, 71475, 46110, 122297, 22635, 34837, 26310, + 53025, 53017, 10622, 90942, 7205, 22145, 163437, 101344, 36189, 355381, 3469, 59647, 36294, 29028, 61676, 33071, + 170779, 1619, 42455, 55588, 21750, 12494, 53664, 106939, 7739, 60501, 600, 42951, 173883, 121950, 75147, 44445, + 75192, 26282, 17177, 6729, 35664, 13478, 22319, 74388, 224240, 51121, 128054, 19973, 113121, 26367, 20959, 71130, + 30181, 27274, 83822, 65840, 26267, 141848, 7294, 161141, 27036, 20489, 14220, 74392, 117827, 12263, 18511, 12425, + 92015, 38371, 93826, 46517, 106516, 24959, 428957, 108509, 55628, 41208, 28538, 6694, 203549, 200020, 130157, 14026, + 67949, 261382, 34954, 75428, 60462, 34936, 69163, 8775, 60844, 95271, 14668, 58597, 35911, 163570, 17395, 41268, + 20457, 77077, 15920, 195151, 1820, 1127, 108523, 1201, 920, 64420, 142690, 3800, 19773, 18589, 25204, 114010, + 8738, 45928, 72305, 27317, 73173, 58181, 4109, 38698, 181993, 2002, 91269, 6577, 38521, 64761, 34725, 2779, + 98254, 99182, 109347, 42999, 76257, 42992, 2481, 76329, 46008, 9716, 174991, 37659, 92796, 26911, 126742, 21977, + 5384, 89414, 18739, 22923, 26868, 2989, 52591, 14973, 151566, 3554, 169141, 41484, 22124, 26749, 78963, 86727, + 2411, 21918, 43055, 36709, 15919, 32188, 39853, 31407, 186872, 106163, 35231, 3970, 180021, 86213, 133789, 47183, + 28099, 10825, 8315, 193036, 152961, 12221, 96811, 33623, 78811, 61925, 91812, 72246, 80237, 171243, 144270, 12504, + 62352, 69843, 208025, 139707, 102653, 182703, 42668, 65058, 74259, 143770, 10084, 32242, 184890, 53802, 20214, 60407, + 16792, 41310, 4184, 1636, 123702, 13335, 68718, 46717, 224945, 64844, 113887, 41497, 29940, 10587, 27431, 128017, + 19512, 17506, 17671, 26070, 75283, 42125, 47504, 37731, 14059, 88044, 36619, 847, 112691, 14770, 55376, 575, + 92811, 347152, 96947, 9385, 233329, 3093, 22326, 45207, 20411, 273167, 31247, 6125, 138569, 8663, 357575, 28073, + 53341, 234780, 21561, 48933, 109802, 48919, 46462, 50800, 50600, 21098, 18940, 1091, 134528, 14935, 2398, 127145, + 66747, 34702, 127805, 27345, 5529, 139548, 51994, 127312, 166531, 11082, 36587, 50668, 31578, 37535, 46230, 2150, + 64732, 41722, 91822, 21109, 67189, 47573, 20129, 8421, 1596, 16448, 126415, 81846, 126357, 140669, 1937, 32338, + 967, 39499, 14778, 48543, 167999, 24888, 12192, 41633, 206598, 60067, 160162, 11609, 109752, 3487, 45910, 15601, + 119431, 19179, 93578, 31236, 207825, 71291, 47437, 21034, 78791, 32425, 31613, 91908, 91938, 6225, 26499, 49240, + 10301, 34970, 12824, 99989, 27311, 35324, 133950, 14043, 24233, 61362, 22243, 35045, 252343, 28863, 12365, 8224, + 28831, 215245, 73325, 83362, 32812, 116785, 100940, 77100, 66002, 61855, 60149, 24654, 112267, 65835, 54563, 141839, + 90895, 174574, 34653, 8453, 8786, 174076, 167014, 20249, 8095, 14050, 68580, 299481, 16824, 48793, 24856, 15716, + 22866, 165280, 33060, 49389, 21813, 47387, 179304, 131281, 60507, 145727, 21710, 16780, 174833, 11187, 19174, 11577, + 19549, 89709, 114442, 11917, 130985, 53665, 52636, 32837, 117051, 78060, 79585, 45117, 52110, 74026, 86227, 52956, + 6938, 48219, 29286, 23852, 81923, 55204, 370875, 58300, 123864, 14993, 25906, 17004, 38061, 191997, 56608, 197099, + 919, 5046, 126484, 79803, 18680, 145935, 124511, 60333, 53534, 6979, 35404, 23791, 46739, 36466, 2445, 19890, + 112893, 35958, 11939, 45333, 161529, 38751, 76585, 129315, 85429, 125900, 37046, 110236, 26761, 13725, 20554, 21155, + 11900, 10186, 81185, 44323, 81121, 127313, 181376, 68138, 91968, 77284, 14617, 15815, 15390, 1425, 15586, 9037, + 217947, 19393, 2643, 291035, 56524, 1195, 154070, 7980, 1713, 2618, 18959, 70645, 6654, 8986, 122964, 149447, + 37089, 79358, 120676, 39867, 85630, 173326, 14161, 103857, 138866, 98205, 107118, 105847, 61850, 48312, 3318, 110656, + 16491, 22884, 29985, 202016, 75577, 7108, 49432, 450007, 16884, 60351, 28287, 31574, 98296, 153369, 5508, 59238, + 73523, 2766, 134247, 6922, 6140, 15761, 20766, 33247, 44645, 98662, 62705, 5296, 6062, 16713, 27012, 204193, + 36366, 4251, 6513, 1097, 29844, 148369, 4030, 44421, 57946, 57215, 45204, 63057, 37932, 100525, 276977, 104126, + 42472, 13150, 108317, 106038, 5266, 1004, 31351, 41691, 20834, 27119, 14871, 42058, 19309, 18264, 15714, 128645, + 33753, 97813, 14991, 36632, 127182, 38788, 23800, 23029, 134259, 141169, 22689, 9008, 35810, 85196, 80190, 175150, + 41805, 96633, 36654, 189935, 45878, 63838, 3242, 5356, 312001, 228710, 66129, 4509, 14881, 203932, 11812, 70030, + 47757, 276830, 122405, 33146, 49251, 2261, 162697, 5363, 120050, 24738, 211941, 21746, 44252, 31697, 2242, 4877, + 3708, 85573, 85060, 82434, 25856, 115291, 56583, 56567, 107864, 962, 58671, 54581, 120347, 39508, 201071, 94108, + 1228, 71194, 12513, 225594, 36550, 6911, 160283, 35838, 41682, 115576, 28022, 16436, 34496, 5034, 74108, 10228, + 47025, 11047, 141530, 3837, 8393, 65028, 55696, 31079, 173365, 61729, 57479, 106029, 246526, 10526, 54647, 134609, + 12894, 3537, 244, 16862, 161607, 118386, 60183, 141700, 35670, 22051, 179401, 24135, 90785, 29822, 122577, 87924, + 126572, 2459, 80584, 28905, 2095, 87804, 54240, 102268, 124731, 60006, 15202, 109796, 157033, 21466, 164665, 37695, + 58694, 81513, 83134, 208222, 554, 5651, 7656, 87297, 12786, 33576, 15075, 146538, 9642, 40949, 163656, 9760, + 4817, 21064, 83245, 14829, 16136, 95061, 68060, 24365, 47864, 1179, 105850, 5322, 174698, 19385, 5399, 111971, + 66992, 363067, 36771, 86468, 4639, 166195, 77004, 80406, 69284, 96401, 199722, 27643, 10625, 105066, 89724, 58878, + 40710, 29791, 24556, 99909, 27763, 9231, 35125, 110086, 51738, 12458, 116193, 41661, 30404, 41774, 96495, 7041, + 264105, 37287, 172797, 19867, 137904, 45042, 61041, 151622, 109882, 58327, 51284, 132939, 52487, 238, 24806, 356262, + 42824, 71570, 114506, 221874, 57514, 290906, 425324, 6771, 2740, 77666, 51262, 18017, 10479, 14457, 11137, 19547, + 146799, 74299, 1986, 193822, 107390, 66292, 13142, 8549, 16586, 41783, 4738, 83585, 88038, 9102, 61338, 33010, + 174951, 5451, 103430, 20873, 9410, 71603, 254445, 29027, 16185, 19139, 109385, 57580, 44158, 18457, 29275, 116743, + 5568, 32928, 91629, 19307, 40658, 229962, 46426, 15411, 46108, 30487, 67181, 20224, 12763, 92267, 69682, 41491, + 97385, 46327, 89571, 20801, 26175, 104473, 82178, 53280, 108859, 90329, 60749, 15258, 664, 104876, 189856, 72942, + 230732, 51261, 34901, 19996, 67470, 20008, 38335, 18089, 46663, 52358, 14286, 59726, 14395, 26243, 124071, 9514, + 50750, 71549, 45061, 44126, 141320, 2803, 58061, 44739, 93140, 659, 64128, 26178, 15361, 168531, 40344, 8977, + 47997, 172770, 68707, 16055, 55784, 23990, 20994, 19090, 6791, 21011, 244531, 47352, 307840, 16546, 63361, 164913, + 118569, 30189, 34941, 229229, 107326, 24202, 160409, 1575, 18056, 16156, 162544, 1298, 58281, 140814, 38998, 4285, + 260415, 19797, 24212, 11490, 54691, 125241, 149765, 53575, 8790, 98579, 39432, 49317, 73332, 17200, 47059, 6532, + 45633, 17042, 110013, 4418, 7511, 26786, 2639, 40536, 45674, 39902, 37280, 138726, 143373, 16114, 16063, 95339, + 14031, 18222, 148011, 134245, 11799, 36311, 103728, 15146, 94491, 24149, 24405, 126162, 35646, 2622, 13619, 190698, + 96544, 59993, 46574, 579, 14560, 43052, 125756, 11698, 26049, 139612, 76126, 94179, 32983, 27506, 5021, 32417, + 25791, 73423, 53795, 119140, 83814, 24222, 419, 60678, 42094, 36193, 71555, 167797, 231370, 39846, 78400, 68056, + 63955, 1124, 59895, 8546, 139212, 47144, 37860, 26891, 2359, 163343, 60583, 105848, 169908, 4972, 13013, 132896, + 3108, 44849, 132211, 4330, 183486, 14009, 10090, 75230, 105867, 102476, 3031, 44769, 28197, 21633, 23419, 68902, + 32941, 109556, 36098, 52255, 124968, 209278, 40772, 6698, 26402, 57023, 171822, 87578, 88267, 23469, 27050, 64577, + 149768, 71917, 89979, 16941, 23053, 7594, 106397, 125192, 3078, 35227, 9172, 18615, 19091, 182038, 12549, 48594, + 52924, 6894, 86017, 20427, 25383, 22580, 75986, 18233, 19209, 61027, 86544, 26111, 111548, 24619, 166688, 24272, + 97361, 51184, 78541, 14792, 3959, 2430, 71174, 280134, 24880, 85091, 19069, 48720, 235061, 148747, 27783, 40579, + 9099, 95152, 259500, 59221, 24921, 73721, 170222, 102157, 161254, 66033, 357515, 82190, 151405, 105610, 28252, 213067, + 20508, 97281, 6878, 87399, 7159, 45662, 182676, 27626, 34381, 71179, 112126, 12802, 20133, 56316, 50576, 70823, + 11434, 14879, 96554, 27582, 74036, 24193, 21984, 147179, 19974, 41451, 8452, 161213, 34769, 115, 18749, 115303, + 36585, 8710, 130627, 54462, 1076, 15711, 78215, 45693, 22454, 41595, 35658, 31785, 17354, 64339, 5699, 30987, + 38727, 113863, 1046, 127166, 235160, 27501, 82135, 137484, 111547, 143478, 71619, 20477, 96454, 65400, 93505, 9234, + 117448, 71966, 130201, 9407, 156940, 10894, 113917, 102178, 91330, 3786, 25046, 137247, 37299, 14204, 156671, 48589, + 7310, 119658, 1019, 3147, 26915, 389655, 28024, 29905, 117060, 110822, 603, 9922, 51369, 186019, 151553, 53930, + 22620, 65936, 33869, 50466, 61861, 18339, 116756, 22544, 322264, 178320, 134504, 32779, 106850, 51259, 7921, 18753, + 111694, 76143, 15475, 39056, 15091, 96327, 38933, 146365, 2624, 6183, 303617, 83865, 40345, 8720, 102137, 208016, + 300446, 153481, 62817, 17230, 177064, 59995, 17444, 96781, 1707, 62069, 105642, 215627, 27389, 113620, 8641, 39778, + 54792, 22640, 92614, 72033, 327783, 56938, 97175, 28337, 132669, 24810, 100695, 42694, 183543, 96612, 26568, 321, + 59003, 67147, 64475, 124682, 17744, 254962, 92433, 55393, 20603, 153319, 316603, 192699, 151134, 16030, 30713, 5369, + 106923, 79389, 15318, 196516, 53084, 229057, 32215, 2061, 71803, 15710, 68210, 36730, 279424, 61974, 109245, 21881, + 319816, 40889, 10178, 55054, 11579, 30821, 76533, 48007, 21946, 12530, 41523, 56504, 16728, 146955, 90643, 77497, + 38274, 58777, 12829, 83673, 72711, 24324, 131406, 209463, 5085, 14864, 2408, 146954, 83391, 104916, 53219, 39654, + 88646, 106083, 13930, 24286, 40159, 28744, 20399, 11792, 25027, 26454, 82556, 24039, 34680, 36361, 145006, 21872, + 10752, 107608, 27995, 36258, 12988, 66287, 75099, 84038, 54126, 38128, 56142, 14292, 30365, 99229, 9312, 5952, + 18338, 50601, 15454, 40761, 100445, 4866, 42787, 168097, 230674, 27, 4416, 59458, 44874, 21538, 13837, 21543, + 84974, 32659, 181908, 81485, 143877, 1443, 22510, 44084, 123253, 114222, 131683, 77045, 139372, 123203, 151023, 23972, + 28082, 30654, 30914, 61473, 91477, 143646, 51334, 8042, 144002, 18818, 47219, 30784, 13096, 53692, 57020, 125132, + 219729, 72133, 94451, 32149, 46016, 5231, 19109, 89053, 50029, 67191, 30812, 104508, 42377, 43699, 106368, 9836, + 14601, 54570, 18766, 12632, 6660, 155889, 71980, 75016, 58244, 83344, 7256, 100628, 58978, 56720, 58199, 118422, + 23918, 11726, 37394, 463, 88206, 139614, 253619, 539, 113611, 38238, 154196, 29350, 64452, 9692, 12873, 4429, + 17541, 32212, 6089, 18497, 41032, 117229, 60868, 14143, 10942, 926, 24793, 66470, 12021, 18956, 23792, 155539, + 49189, 11284, 84405, 157831, 10978, 12543, 64410, 50098, 40175, 82131, 32892, 21615, 37156, 5526, 99592, 36215, + 10947, 19241, 20602, 2093, 71709, 93588, 80808, 10971, 106894, 25921, 413, 34040, 112538, 180819, 118821, 72357, + 57007, 79329, 16870, 137412, 137486, 10245, 90727, 18898, 150608, 14622, 19833, 22840, 152719, 29427, 209294, 4232, + 40615, 60643, 170375, 22011, 7746, 28136, 332881, 60551, 279716, 193813, 38074, 19946, 13101, 16840, 117701, 27751, + 19524, 59518, 5857, 368, 28708, 105821, 12973, 27739, 40578, 900, 41397, 104380, 72320, 33862, 8409, 34652, + 1096, 35868, 72140, 8303, 182051, 82682, 33389, 5630, 94527, 27756, 204584, 39519, 51275, 31654, 10240, 28759, + 22833, 178542, 47192, 48182, 45164, 83416, 42256, 42796, 81917, 217466, 53292, 37786, 77519, 106347, 83381, 18672, + 48508, 13787, 77506, 13385, 5421, 76619, 372545, 27228, 140302, 83313, 3227, 42955, 37845, 66043, 76055, 149143, + 149830, 12497, 9759, 138621, 5587, 153959, 83576, 136204, 27579, 39401, 30659, 75311, 5357, 6559, 74434, 7707, + 428725, 26547, 2082, 18025, 248187, 41435, 176983, 82585, 6326, 238794, 27806, 33103, 206760, 30220, 62067, 73068, + 39814, 3267, 31130, 1487, 32585, 16095, 47315, 334742, 89923, 102036, 75915, 77001, 44341, 23722, 4933, 28107, + 288753, 33496, 67090, 13693, 284443, 67130, 6821, 12171, 96368, 120123, 128906, 6889, 31201, 197218, 124216, 25556, + 94189, 226026, 49191, 116420, 119504, 22368, 28238, 62479, 20359, 140859, 29908, 42319, 52073, 25021, 11717, 171363, + 103216, 48554, 148106, 44322, 179, 62550, 142748, 5200, 27934, 626834, 53683, 40353, 32801, 386580, 59130, 42350, + 96035, 956, 88884, 71218, 34111, 41335, 31551, 1556, 34309, 7435, 32506, 89091, 101326, 35050, 97836, 7566, + 18198, 14509, 235440, 30012, 20704, 338945, 90305, 62331, 210266, 5359, 86970, 67633, 37643, 51918, 7476, 35122, + 27880, 2530, 23516, 55992, 141873, 9269, 20887, 235173, 106000, 53315, 71177, 78367, 19414, 8455, 3948, 72358, + 56614, 93522, 50567, 6412, 167714, 32465, 101863, 1914, 66483, 142566, 61810, 14328, 107885, 75527, 21510, 22073, + 86602, 3162, 170297, 80142, 4379, 139776, 150756, 52344, 20796, 126580, 47459, 31811, 75467, 203428, 2360, 109945, + 4987, 40280, 38609, 247457, 5017, 131195, 52873, 51358, 118857, 25612, 54684, 86642, 26003, 82237, 10347, 74817, + 34308, 134385, 105661, 2079, 114428, 3924, 56947, 20197, 29198, 93080, 30441, 23003, 6686, 189968, 44029, 59712, + 29697, 69462, 47863, 6319, 73632, 71419, 54022, 228432, 3739, 11617, 144267, 6304, 69795, 159284, 38182, 88987, + 16798, 60652, 18367, 39753, 41504, 26776, 44767, 4986, 7207, 326091, 10211, 275129, 30722, 15983, 114324, 26287, + 21436, 250022, 386, 16493, 36735, 47994, 4425, 57498, 28067, 7086, 86124, 96341, 28545, 29897, 71934, 19803, + 3239, 94102, 112964, 21957, 11221, 53105, 41589, 82164, 36031, 6367, 42771, 2307, 41889, 128904, 54967, 59098, + 100010, 163061, 65256, 39405, 19247, 129504, 97081, 10279, 317673, 79950, 84866, 47576, 29495, 35727, 17138, 23769, + 174554, 168948, 28307, 137478, 6424, 65666, 84059, 28007, 129725, 112584, 87500, 22631, 53845, 9237, 125865, 12109, + 94986, 62791, 47377, 95747, 7955, 119822, 43499, 77478, 59676, 37816, 112528, 83870, 2604, 10721, 277540, 129593, + 191497, 1803, 103962, 39100, 19735, 137806, 184562, 831, 102214, 21611, 10860, 96243, 62954, 12392, 277571, 104806, + 23844, 21269, 30123, 51663, 11872, 3731, 70610, 110093, 179525, 50391, 26607, 87825, 261436, 17108, 19172, 65210, + 34492, 179038, 18937, 8799, 428, 29645, 11956, 61342, 78404, 376484, 132083, 73837, 142035, 103650, 20615, 4466, + 16747, 74934, 38480, 234599, 17246, 46547, 32844, 24552, 27578, 22737, 103773, 39027, 37021, 1234, 22307, 95862, + 33672, 4191, 11010, 27369, 57944, 36384, 94490, 7931, 26056, 163500, 146122, 22564, 135760, 93787, 61065, 30077, + 2369, 6137, 12659, 3122, 61674, 56540, 24935, 25675, 122066, 26194, 26305, 22069, 31327, 2064, 15705, 149614, + 19374, 89531, 613, 93086, 157065, 5730, 15360, 6683, 40553, 8430, 74835, 94791, 130982, 74032, 11372, 90140, + 69619, 36036, 16092, 112362, 71290, 44790, 23930, 155440, 38855, 195955, 61949, 49611, 72100, 9710, 26268, 41136, + 92903, 169781, 27353, 78082, 95940, 112981, 249266, 45995, 51422, 17889, 6210, 74226, 165999, 87787, 28659, 84558, + 65713, 42221, 17212, 99031, 57873, 122295, 227056, 76534, 50726, 57460, 287606, 77186, 7288, 29042, 88166, 172092, + 20272, 22733, 128506, 113493, 2081, 55443, 102934, 214, 42326, 28948, 53196, 24237, 22624, 21099, 13480, 39377, + 81120, 35325, 45300, 24047, 57914, 47609, 64670, 25672, 79352, 7747, 71834, 161803, 19447, 8688, 10183, 9684, + 1684, 6277, 61421, 45761, 72302, 118558, 18353, 10661, 11774, 128325, 16327, 2665, 302559, 70280, 76546, 45579, + 161481, 169457, 36438, 37410, 96396, 127007, 10776, 56760, 13692, 115406, 41747, 83908, 414988, 69549, 169745, 58040, + 3721, 62350, 104731, 13605, 79066, 14490, 121161, 108219, 56627, 83538, 32335, 35780, 46883, 23245, 40346, 24451, + 21150, 129629, 31758, 47729, 11747, 2392, 5660, 43534, 12184, 23309, 97227, 201922, 5856, 75935, 22492, 245478, + 113458, 122567, 38892, 52163, 176117, 98436, 387939, 127565, 84416, 26809, 1689, 44206, 52079, 78841, 20795, 5683, + 27933, 162169, 34126, 12822, 3354, 45811, 72520, 20811, 59765, 13615, 3254, 29527, 141359, 123305, 19887, 90838, + 2212, 8885, 33750, 29379, 216309, 13657, 7475, 88895, 2555, 55375, 35969, 66537, 23458, 112987, 1751, 75280, + 196722, 96722, 67717, 118130, 142463, 83824, 80129, 105478, 45701, 183568, 315287, 14884, 44548, 167199, 36212, 100715, + 28798, 95743, 42919, 6271, 19418, 59193, 16434, 72701, 215, 108179, 34472, 75818, 29916, 15862, 29177, 1351, + 9396, 129616, 4305, 86650, 10574, 51218, 914, 206197, 114226, 53103, 156910, 12946, 84475, 16322, 71666, 47108, + 13520, 81329, 27088, 120745, 18694, 174187, 3645, 72390, 34056, 18867, 220604, 95316, 4524, 97988, 41515, 586619, + 90302, 23520, 19632, 127752, 62930, 258836, 36988, 204585, 13539, 57180, 13517, 6044, 19407, 65336, 268952, 132299, + 77209, 53483, 3327, 22672, 7728, 50216, 2729, 12196, 38088, 36872, 5799, 111465, 9535, 11303, 51899, 76725, + 2263, 23913, 3675, 253827, 23875, 65387, 63019, 12817, 183945, 28678, 43266, 62072, 17750, 269599, 29961, 5765, + 26274, 6555, 2446, 55197, 67172, 1910, 71875, 19799, 10585, 1419, 27911, 88939, 28042, 167002, 124915, 104112, + 22199, 47768, 14066, 16710, 7478, 99068, 196517, 131507, 51331, 27291, 42046, 63842, 66030, 117306, 144818, 41353, + 26774, 14822, 38660, 171065, 192929, 121185, 116712, 28895, 31434, 3911, 52612, 111118, 25850, 18697, 65634, 4147, + 50197, 74729, 15097, 117548, 52926, 274499, 54590, 79384, 178158, 113803, 36365, 137334, 4679, 5949, 253573, 27681, + 181256, 356354, 65776, 146248, 70184, 2871, 18045, 156661, 229600, 6542, 22726, 9001, 9959, 34743, 33915, 7460, + 105594, 269690, 12482, 86077, 72158, 12017, 58753, 24594, 73974, 3029, 1912, 30079, 2726, 109412, 146145, 35326, + 35085, 862, 90862, 85609, 78087, 43053, 160170, 33043, 23284, 4515, 162825, 69896, 35568, 601, 13016, 1407, + 51713, 90134, 750, 45520, 155676, 21397, 168585, 187237, 5401, 125230, 5635, 89220, 27254, 54715, 98930, 113085, + 11966, 3030, 1855, 149700, 17569, 56634, 16775, 51586, 223253, 10938, 121033, 70787, 71993, 76450, 39521, 26162, + 103357, 94057, 56597, 26906, 111477, 293134, 42368, 24553, 55722, 30882, 11930, 19889, 30504, 35653, 6466, 203139, + 26034, 287857, 19452, 2522, 46774, 8228, 76457, 83553, 35392, 6216, 12166, 56704, 36285, 6768, 54803, 1726, + 214814, 6895, 182419, 26778, 41143, 53690, 13669, 45646, 163465, 22665, 198804, 39125, 1051, 54093, 61411, 31560, + 16094, 26798, 90341, 277777, 81044, 169520, 129829, 46588, 6636, 71429, 29098, 27473, 76489, 47101, 118137, 125121, + 179102, 29265, 57351, 60270, 20712, 59437, 33382, 18626, 39178, 70695, 80048, 54642, 35683, 106381, 97513, 43264, + 125177, 120906, 35533, 22522, 54219, 7788, 92290, 6116, 30617, 6801, 86129, 39209, 52994, 53661, 59735, 17738, + 25324, 24278, 105977, 13689, 50123, 36059, 130088, 54180, 2543, 36656, 87050, 59769, 87529, 20220, 367, 68705, + 58995, 26101, 26380, 43246, 10688, 79793, 82063, 59968, 125199, 31463, 19802, 62223, 12388, 70063, 151361, 3296, + 60158, 33268, 27121, 110554, 125481, 31240, 69489, 60334, 131646, 25391, 20034, 24248, 7642, 55281, 33709, 57581, + 133350, 77700, 27095, 3522, 65874, 30518, 61307, 126098, 3438, 49052, 9849, 78050, 97277, 50748, 175256, 49826, + 101450, 107315, 118984, 13409, 10075, 128877, 62205, 13193, 56344, 25228, 87810, 2143, 116821, 7648, 113840, 19459, + 50778, 131885, 88512, 13697, 60547, 58403, 210177, 34494, 98016, 51781, 47807, 12099, 106135, 16443, 16925, 19635, + 13859, 8422, 14030, 4756, 14255, 48634, 3275, 4837, 16300, 230472, 6616, 53129, 77373, 22360, 111581, 9662, + 173521, 71655, 15044, 5531, 8285, 190633, 62896, 54909, 45932, 34330, 16255, 17909, 37426, 152464, 256859, 18903, + 4054, 67227, 5705, 135855, 114295, 14380, 28822, 86386, 55947, 44796, 22159, 43163, 7703, 65450, 5829, 97182, + 39114, 652, 2216, 44468, 52, 74475, 73693, 208207, 51119, 111015, 105280, 42780, 128135, 3956, 13974, 30409, + 19714, 40616, 22185, 44115, 60715, 199079, 86742, 81192, 9554, 53876, 58171, 29597, 50492, 316379, 10539, 3453, + 88180, 23111, 24529, 93240, 2823, 46332, 22213, 8752, 118271, 197846, 6618, 8946, 52993, 21325, 30302, 17074, + 122625, 9575, 29441, 295253, 97919, 3130, 132791, 140156, 23859, 8941, 106857, 22772, 37895, 107740, 9471, 34989, + 25040, 85180, 21330, 47109, 33614, 110324, 23189, 24151, 32102, 171390, 19981, 29005, 20431, 121, 38106, 170174, + 3577, 46060, 182390, 13411, 9275, 119138, 47329, 30160, 15686, 30347, 7585, 10003, 43031, 29151, 20512, 144355, + 157741, 153623, 16851, 99315, 110358, 156059, 69556, 9859, 1884, 75126, 4225, 180276, 40291, 131485, 17863, 1299, + 125391, 75039, 111409, 31614, 13736, 31156, 97629, 65733, 5008, 14589, 129738, 29549, 64881, 29351, 75196, 52675, + 87336, 57594, 21161, 14655, 77381, 35333, 37937, 262082, 70711, 100777, 11065, 52574, 43032, 79308, 11911, 5569, + 49155, 8990, 20956, 71672, 118587, 90936, 6794, 2889, 70494, 14885, 17291, 20073, 4318, 33042, 38735, 27931, + 10168, 11340, 174780, 29799, 30126, 32276, 416159, 9138, 12580, 186182, 69114, 30093, 10524, 55369, 90592, 23723, + 280104, 31769, 43457, 134915, 104001, 3107, 52049, 3483, 145413, 4347, 87847, 8340, 2862, 22905, 12749, 10655, + 84140, 32339, 14853, 21123, 6603, 75082, 30462, 29877, 106005, 84964, 69112, 129634, 13566, 31377, 1731, 2591, + 12780, 75605, 9265, 203857, 11251, 95054, 43621, 106786, 42830, 115761, 76779, 15968, 571, 316548, 48436, 23152, + 179910, 24939, 4039, 62740, 82443, 162336, 105433, 153188, 13146, 12020, 11190, 145468, 469, 151738, 6924, 16613, + 42714, 25880, 5783, 38804, 32591, 110905, 81649, 189448, 265217, 122177, 28046, 8852, 424024, 1774, 13702, 37891, + 92553, 66876, 68996, 31394, 54721, 100409, 93602, 51349, 134100, 42960, 121568, 58272, 6007, 12605, 20028, 3624, + 15242, 25008, 65373, 95897, 114681, 115646, 2589, 33333, 59030, 148878, 4427, 719, 16718, 23118, 3261, 37212, + 85465, 55213, 20762, 7510, 200214, 136975, 141829, 8623, 85982, 9053, 8985, 13680, 55174, 20625, 8519, 15392, + 165013, 16648, 8679, 27707, 23493, 74409, 23572, 32138, 56964, 21537, 197403, 32462, 82529, 23420, 28463, 4528, + 109150, 117327, 76538, 9244, 32706, 84770, 24954, 49185, 27568, 3481, 35176, 25954, 82442, 152974, 131562, 69937, + 5350, 25825, 141497, 121347, 14976, 75327, 17713, 2839, 13165, 257262, 30030, 30105, 44890, 162261, 56625, 19734, + 60021, 19579, 1465, 101402, 21343, 50719, 82005, 23880, 33978, 2744, 4244, 16973, 17264, 25584, 4273, 85481, + 4655, 19471, 172622, 36425, 22328, 212066, 128477, 64373, 27819, 33935, 83439, 54538, 75730, 73945, 182416, 338, + 16567, 164442, 82351, 56235, 55483, 38729, 47137, 36504, 14510, 39166, 16573, 4712, 17926, 119742, 48289, 74781, + 45827, 314393, 143249, 63030, 150609, 33960, 254056, 83767, 3704, 81354, 45727, 6473, 7385, 36244, 6886, 18673, + 272531, 4187, 62156, 112398, 161543, 82887, 4358, 87142, 76904, 76583, 39823, 167961, 122163, 68178, 11770, 14478, + 52405, 50115, 29516, 109139, 2039, 4206, 65909, 23385, 19165, 89405, 28262, 22275, 41623, 3099, 70734, 12924, + 14423, 41773, 25426, 95066, 228354, 10150, 40311, 18456, 3369, 167019, 217588, 126793, 176360, 66455, 4269, 8444, + 85491, 121695, 17697, 323, 7122, 20991, 35726, 50184, 35789, 94066, 146437, 243045, 303724, 21794, 8433, 198209, + 4465, 23672, 80873, 33604, 13628, 46964, 2602, 33500, 2233, 8434, 6196, 25551, 55311, 64859, 90756, 733, + 118771, 16152, 16282, 13527, 20713, 42651, 69883, 78249, 10006, 70583, 164285, 102376, 221519, 42660, 9468, 65430, + 45115, 136780, 41566, 157119, 71021, 40395, 88297, 10249, 35650, 41778, 28731, 28138, 29775, 49179, 39391, 51182, + 7337, 14843, 4441, 103029, 10864, 81753, 72912, 49213, 20665, 88374, 112909, 1667, 21142, 63823, 38287, 19613, + 1746, 41069, 30542, 41967, 15080, 138315, 9822, 40857, 1624, 120146, 62254, 46115, 32449, 11046, 21374, 514828, + 10905, 260390, 38829, 21553, 105743, 7303, 96235, 38405, 229797, 32678, 23538, 112753, 7701, 37587, 64813, 15914, + 3940, 40782, 259364, 20373, 22997, 77967, 19173, 76602, 178467, 82126, 9044, 83531, 57208, 74018, 5950, 34656, + 389057, 21826, 6662, 16035, 39683, 55167, 129407, 79420, 59403, 152449, 39047, 31506, 63344, 27006, 12334, 147213, + 63125, 155934, 26422, 197447, 54847, 124681, 52392, 3641, 69691, 15548, 83724, 62974, 18336, 43641, 194003, 56605, + 56448, 6561, 195097, 103908, 3362, 8507, 99274, 120393, 37202, 12934, 69852, 54075, 18282, 7789, 50160, 102080, + 29648, 97272, 47381, 12391, 138224, 47286, 208664, 50910, 35867, 32185, 28804, 64164, 10495, 11850, 159760, 137513, + 5911, 76063, 12977, 6056, 28814, 21821, 2163, 130, 26653, 229563, 675, 34076, 31514, 47917, 92810, 44791, + 176702, 25297, 80044, 28279, 26550, 62323, 9943, 101265, 45621, 173758, 88568, 219069, 11734, 117073, 111186, 26075, + 4525, 39923, 16003, 12712, 40543, 7197, 150583, 16316, 73944, 199805, 158502, 7166, 121080, 2343, 53537, 17725, + 27858, 14692, 138991, 22323, 155561, 72448, 37087, 173360, 14887, 2310, 89844, 54066, 44670, 35610, 30471, 49008, + 30742, 32492, 123549, 16741, 8796, 69544, 57441, 97055, 107455, 22125, 10594, 123866, 113472, 2733, 85686, 54673, + 56369, 34761, 5044, 12915, 75581, 8965, 47647, 30073, 183777, 13677, 34414, 87158, 240095, 56678, 23997, 13674, + 133699, 17662, 364, 13753, 153299, 27177, 51527, 30243, 8768, 26167, 16767, 50595, 160464, 166312, 23739, 14534, + 26058, 9664, 63302, 110621, 49078, 86820, 10195, 18754, 103971, 41541, 46431, 27835, 21875, 167947, 172353, 12902, + 71486, 20686, 45374, 12571, 44888, 12274, 1818, 10422, 17156, 10122, 31744, 9367, 9678, 87337, 19033, 70558, + 89541, 21373, 2670, 9033, 123019, 13271, 234210, 43826, 102337, 11809, 135892, 7723, 3972, 64409, 19618, 54008, + 83930, 155668, 38822, 37966, 21245, 24138, 260, 246255, 87852, 28211, 156411, 8088, 109660, 68896, 82086, 248065, + 287918, 183132, 99271, 104331, 183019, 20735, 38511, 16336, 686, 18533, 18914, 36568, 10100, 17413, 11801, 17493, + 39177, 49978, 80098, 133024, 283941, 8179, 153303, 913, 11274, 22090, 73741, 81799, 24736, 36017, 34397, 5355, + 26793, 74880, 144578, 239455, 26214, 19233, 17629, 106193, 25995, 57924, 89963, 116991, 77011, 261582, 364267, 12039, + 141580, 15178, 36187, 9064, 4070, 21836, 104740, 12532, 23742, 192159, 139401, 14516, 46285, 50127, 9705, 30183, + 46632, 6312, 66032, 10073, 30700, 26025, 26702, 43421, 26669, 6136, 155289, 120269, 19056, 202531, 43062, 10321, + 35951, 149425, 302834, 15999, 115575, 92927, 51885, 95094, 174034, 1831, 20175, 39292, 56097, 9329, 155235, 20052, + 35463, 55521, 17719, 122027, 87425, 145479, 31818, 5229, 24575, 132139, 118737, 52992, 44245, 16168, 78384, 56556, + 38701, 11367, 88487, 19022, 82317, 214446, 53146, 132874, 85922, 28449, 40982, 81866, 281616, 112901, 26578, 190706, + 100333, 155311, 101029, 171716, 147697, 12430, 68023, 26065, 61503, 69034, 60721, 126933, 7730, 7965, 21463, 59048, + 84330, 17699, 17875, 37832, 8530, 54375, 218360, 53773, 59917, 9867, 92197, 54218, 61597, 39007, 87092, 58775, + 17173, 53529, 33744, 101641, 9092, 6126, 34354, 17856, 32658, 23212, 16624, 40012, 90288, 66804, 30957, 193996, + 193136, 3361, 126541, 62118, 39023, 18809, 8034, 19719, 20381, 66386, 64493, 20206, 56654, 11892, 180795, 70430, + 31132, 148921, 124862, 23413, 7779, 38708, 40301, 16544, 1919, 80033, 29947, 93475, 1375, 135168, 156926, 69211, + 117128, 57078, 75276, 39285, 30819, 18464, 3044, 51097, 11169, 214069, 300112, 18592, 40938, 132884, 51336, 55473, + 23935, 202263, 99605, 7252, 115201, 18984, 268130, 87746, 101155, 21993, 7612, 2978, 151034, 53745, 151729, 174929, + 4835, 64678, 53387, 27068, 11231, 14136, 30257, 163776, 74550, 15754, 8669, 6350, 89388, 45349, 422995, 68021, + 59951, 87642, 86425, 54667, 91704, 28427, 56079, 64527, 107312, 2367, 6715, 32058, 167882, 83377, 9472, 24984, + 115062, 35722, 33140, 156862, 12732, 24084, 23697, 34539, 72738, 20672, 102578, 11210, 88703, 7244, 19853, 19168, + 464019, 27128, 46941, 50269, 158267, 8850, 158112, 51669, 57995, 41368, 58379, 14134, 60496, 91738, 13630, 44359, + 737, 15344, 120328, 46261, 14371, 8214, 53796, 49253, 123867, 56387, 104801, 7333, 4174, 48503, 43922, 3083, + 243339, 116418, 479757, 153147, 159946, 19349, 47019, 17868, 7568, 17831, 7985, 56769, 16025, 112323, 7079, 40969, + 134556, 11297, 18538, 58669, 110916, 153620, 73377, 72354, 38103, 205536, 68495, 102706, 191, 10869, 164292, 31753, + 80226, 87342, 114379, 12760, 88794, 19334, 85112, 20828, 29688, 22880, 32405, 3197, 27230, 29826, 77087, 46535, + 10454, 11432, 110215, 23620, 76308, 72189, 116329, 168613, 57647, 19673, 10378, 1049, 77409, 28757, 24133, 588, + 113483, 16684, 61242, 31088, 66864, 24674, 161602, 3529, 14745, 90530, 299150, 6673, 19808, 84006, 14057, 114223, + 12023, 167545, 57708, 91489, 46583, 15662, 2782, 13163, 84805, 1309, 47528, 68166, 16015, 48871, 44523, 145426, + 17102, 65184, 54856, 101626, 2231, 162868, 38087, 134570, 20611, 72893, 296437, 103821, 3547, 51502, 32402, 63371, + 95740, 8947, 63165, 25224, 250131, 70323, 10235, 39906, 34559, 51697, 134092, 90702, 108894, 201322, 13521, 98255, + 8498, 173210, 61323, 5939, 15853, 2071, 83348, 11131, 159169, 47234, 2625, 1728, 148920, 59236, 14351, 20915, + 20942, 19005, 8569, 220082, 2813, 129877, 76369, 208632, 93160, 15477, 19266, 71454, 45188, 37118, 21981, 734, + 210613, 24054, 1267, 258926, 45531, 14333, 1358, 4214, 52587, 73176, 70405, 3934, 149062, 67102, 129336, 24604, + 39782, 144525, 88004, 81838, 28194, 51093, 36216, 42928, 57849, 8118, 2715, 191067, 60965, 105811, 65180, 7052, + 84954, 70694, 46912, 219608, 89766, 22029, 26626, 102536, 84453, 50777, 25605, 105083, 100927, 20688, 87599, 26842, + 16501, 4589, 1582, 37485, 27658, 50645, 120746, 2335, 165311, 11419, 118946, 1635, 103841, 81324, 26376, 135646, + 54192, 116632, 21545, 33403, 207341, 58353, 177692, 33129, 19558, 9632, 75823, 7780, 20084, 107884, 116296, 109946, + 319622, 58315, 14925, 134360, 5672, 15528, 113198, 68474, 205467, 66116, 49681, 2705, 98462, 83417, 21258, 159469, + 61849, 81586, 62636, 15482, 36279, 20980, 9940, 193129, 13609, 130807, 18949, 73964, 147177, 131897, 86637, 146769, + 24726, 30328, 30775, 29789, 165015, 16356, 4333, 5505, 209489, 79847, 8748, 132099, 59591, 103870, 50045, 162834, + 31157, 71923, 122346, 6112, 6551, 139841, 45179, 43676, 117580, 19506, 44727, 106994, 75060, 69628, 17203, 46010, + 141146, 9659, 247052, 66602, 277310, 21659, 46258, 176126, 21072, 87, 20184, 63737, 22023, 124145, 55015, 107649, + 106474, 147290, 65612, 13076, 63041, 16396, 150430, 62688, 137443, 6987, 49604, 88814, 122965, 88723, 27058, 177180, + 68371, 34502, 30567, 11200, 5383, 48204, 26504, 19554, 42146, 47062, 6975, 51017, 98961, 25976, 71879, 161741, + 113467, 13050, 91074, 277058, 30863, 61884, 41533, 46948, 23794, 16521, 149829, 35815, 4843, 40881, 56017, 95769, + 99630, 72286, 99851, 13623, 30392, 51474, 63363, 63865, 82679, 1059, 168866, 25195, 13699, 121522, 234449, 35601, + 241612, 30212, 73616, 264919, 33601, 161573, 60734, 72643, 93146, 104874, 19083, 97309, 24319, 146272, 53100, 87181, + 18643, 3074, 12143, 84691, 32155, 10902, 38113, 83987, 95669, 22320, 37308, 44763, 40440, 203540, 152769, 7319, + 15333, 37687, 43812, 63607, 34089, 899, 246178, 71268, 67799, 16016, 114972, 58528, 142144, 3955, 144552, 72635, + 58245, 136701, 104014, 243, 38633, 62199, 14295, 9747, 114531, 27309, 21640, 159861, 117400, 124053, 13195, 210463, + 77861, 81073, 239628, 226797, 188726, 25428, 49381, 139825, 5507, 45355, 15269, 48541, 2568, 12101, 40308, 1768, + 8853, 78278, 55853, 27498, 10987, 12866, 22855, 16207, 107222, 28940, 68976, 28505, 2663, 277982, 71506, 191712, + 2421, 165066, 37699, 52827, 11530, 112085, 187070, 14784, 13345, 2370, 197969, 71689, 30075, 93786, 97183, 71992, + 41785, 19656, 26541, 5218, 118661, 37497, 14909, 185795, 104786, 64176, 31138, 67561, 17459, 21130, 111703, 11368, + 12490, 45880, 38409, 147530, 16281, 12336, 20898, 10505, 71936, 39455, 49254, 62813, 193555, 86430, 18811, 97787, + 17431, 50448, 85973, 4047, 5944, 9900, 65788, 238170, 71758, 45771, 89284, 65578, 26485, 49627, 32381, 33713, + 77317, 8559, 35413, 14870, 20803, 34468, 81897, 94234, 367167, 24080, 137854, 191387, 158, 7578, 65751, 15809, + 7362, 17010, 196493, 65502, 93430, 391382, 2879, 10420, 11735, 7147, 23542, 17615, 172445, 156086, 37413, 42670, + 46002, 31761, 57780, 41672, 11532, 25360, 90866, 49967, 54482, 3553, 67022, 173415, 930, 48911, 25321, 44848, + 62911, 34519, 229774, 187702, 2235, 26813, 21693, 1315, 23004, 97752, 23681, 170907, 179236, 168028, 11780, 33446, + 4764, 8196, 13633, 286646, 101859, 29094, 37084, 18677, 208113, 11037, 67253, 68845, 22477, 60395, 22179, 83654, + 55163, 30814, 111690, 84894, 95579, 111070, 15123, 2301, 14098, 14628, 22693, 64944, 67320, 32427, 113228, 8450, + 162556, 30175, 61058, 80543, 90709, 143529, 88741, 208523, 156949, 1923, 33966, 23151, 3826, 241299, 16138, 83350, + 57492, 27183, 107353, 138052, 4025, 107597, 35297, 67773, 34092, 30452, 43300, 6957, 87442, 94684, 16965, 217438, + 104565, 70559, 98891, 21648, 6718, 16784, 149691, 99066, 186015, 19497, 66551, 37693, 28214, 16720, 64083, 40532, + 14209, 87486, 1612, 145702, 10039, 70355, 14323, 130951, 107186, 119516, 74814, 104148, 233912, 48066, 30803, 17404, + 58877, 26118, 50223, 44594, 81637, 205665, 99360, 81833, 55265, 26920, 28438, 30781, 39828, 1038, 31826, 48903, + 6194, 56604, 14761, 59828, 145813, 74771, 74706, 51758, 50831, 37050, 3597, 24506, 105849, 6593, 4154, 16139, + 4974, 46766, 28473, 30674, 88319, 27775, 32504, 6677, 122296, 25830, 25628, 152679, 10272, 18637, 3167, 49269, + 197216, 13892, 17101, 74035, 95714, 67486, 53321, 82319, 51540, 39761, 17803, 187333, 72418, 71349, 30143, 35120, + 23324, 149892, 42804, 9890, 91555, 30670, 7507, 27360, 8743, 12725, 15462, 94244, 140452, 44821, 17416, 38926, + 250249, 54572, 82822, 54752, 51666, 63387, 47442, 57021, 34124, 37290, 40715, 29430, 7229, 111417, 75006, 22299, + 38592, 3207, 31696, 25882, 129641, 85221, 119327, 11951, 78169, 25237, 51044, 149983, 174242, 9947, 220995, 4324, + 22464, 397659, 78193, 25301, 149964, 59306, 234039, 11815, 51450, 116927, 58974, 159239, 14034, 75956, 10213, 91547, + 10026, 88574, 19060, 33083, 95376, 47430, 31034, 61653, 26190, 36085, 5131, 14374, 120062, 15192, 280008, 9263, + 14401, 19099, 200440, 66652, 8700, 156222, 62663, 66966, 265, 110, 148040, 36034, 31386, 104323, 17822, 32638, + 143573, 164335, 16580, 50402, 7203, 38721, 213812, 21515, 229889, 8504, 38602, 75516, 61567, 60579, 12745, 46326, + 4227, 18582, 60229, 59397, 140981, 39037, 55638, 17735, 2466, 3755, 51288, 30552, 72052, 186323, 70031, 82764, + 10787, 256, 117464, 143130, 10062, 6313, 63167, 28509, 30958, 1511, 26452, 130270, 6099, 62843, 2008, 134723, + 38471, 103714, 11981, 137269, 30103, 21650, 155870, 27623, 23202, 21416, 31748, 136202, 208101, 42177, 21612, 97179, + 70847, 80823, 26151, 15957, 467, 19669, 80201, 152985, 58934, 49413, 43187, 165152, 32271, 3413, 278897, 95326, + 32984, 22407, 4165, 5889, 36637, 54267, 154498, 84424, 24107, 32263, 13642, 61899, 30771, 48906, 53541, 77288, + 17109, 68812, 133945, 23919, 73353, 73829, 91032, 251994, 13650, 62276, 107145, 232161, 2098, 1645, 1664, 247395, + 157040, 42258, 5942, 117930, 67366, 16060, 9794, 122685, 66904, 16976, 197964, 13983, 106018, 68009, 103583, 28958, + 265380, 17355, 73225, 43935, 107238, 21443, 155998, 64685, 18535, 31098, 26652, 188152, 44025, 21291, 51390, 24741, + 32681, 22989, 67962, 69432, 144983, 171068, 156235, 7891, 62505, 30254, 83172, 66755, 91295, 123868, 35802, 115707, + 56120, 334807, 135497, 21871, 3082, 226529, 127778, 48841, 77508, 143672, 108714, 27565, 10322, 144014, 44830, 149778, + 63023, 9719, 13437, 27943, 36700, 13695, 163539, 196344, 81885, 30099, 44647, 4703, 224127, 11553, 28255, 159827, + 16721, 24326, 85789, 18228, 45023, 10808, 22936, 17273, 239261, 46240, 15558, 55286, 111272, 53778, 10007, 200688, + 13852, 33199, 25937, 118127, 7866, 95568, 13550, 69075, 149243, 18187, 18054, 139272, 204199, 48032, 9916, 53168, + 32309, 66646, 20390, 30523, 22084, 55674, 32559, 215681, 42029, 99514, 103068, 63726, 38316, 8856, 122667, 9308, + 126644, 295281, 11559, 40999, 104973, 114406, 69105, 9022, 14406, 80819, 104640, 60160, 43454, 8575, 34276, 11096, + 67322, 37022, 36926, 101052, 61310, 36620, 61086, 109693, 15789, 9610, 221009, 16189, 40285, 3194, 57111, 7696, + 24026, 1071, 17787, 219517, 181047, 102229, 1436, 19143, 6301, 110183, 37601, 45487, 70927, 56572, 105459, 74084, + 23319, 69989, 91217, 16551, 115823, 99155, 38977, 40934, 27248, 94397, 86590, 107504, 66693, 29641, 1379, 47255, + 115875, 1054, 8435, 39144, 278566, 3140, 317123, 121774, 63007, 54, 8414, 27632, 146844, 17916, 144167, 46464, + 56841, 9985, 60753, 54973, 59007, 15854, 105030, 302270, 87368, 102284, 52117, 2320, 180001, 24004, 45415, 28122, + 22370, 12080, 4179, 143103, 42114, 5196, 9147, 23819, 80605, 58583, 158409, 10286, 12022, 7119, 150321, 118598, + 10374, 25544, 101645, 10354, 308, 97195, 61157, 56511, 25079, 3266, 28236, 118492, 14689, 20295, 135126, 19093, + 12618, 57448, 107655, 29480, 63368, 199518, 134395, 42712, 7936, 62939, 58228, 35501, 264973, 47880, 112138, 63936, + 212291, 63680, 36241, 9561, 136713, 9208, 3926, 120889, 95999, 43551, 83774, 6921, 105801, 11525, 3247, 91697, + 18965, 18822, 61436, 115290, 32075, 47003, 24387, 26636, 48700, 190949, 19812, 48361, 52230, 62488, 108527, 105631, + 35119, 118159, 8412, 2552, 96912, 124705, 45876, 32587, 32992, 107747, 77489, 51983, 8586, 88880, 11803, 52063, + 16606, 162643, 143626, 89658, 101333, 22654, 101310, 38641, 101812, 20259, 123750, 2503, 14969, 219100, 8690, 57801, + 39930, 59910, 37399, 71781, 759, 10810, 116498, 88252, 193090, 2214, 139472, 14511, 27387, 12596, 1241, 7718, + 42914, 11603, 116092, 73428, 12937, 23266, 15835, 53439, 5058, 18649, 34255, 102275, 62646, 29092, 74301, 111969, + 64528, 103339, 89133, 263917, 38624, 31458, 186803, 51532, 25743, 71285, 12736, 12343, 37502, 180824, 143025, 172311, + 3716, 6203, 6498, 22229, 4435, 2166, 66689, 87857, 30352, 26521, 32385, 19406, 178687, 47754, 51273, 121646, + 26461, 8198, 36440, 4640, 132611, 45114, 31837, 69521, 42002, 24437, 25080, 46669, 138442, 89271, 46945, 24420, + 35833, 124503, 8025, 46899, 59582, 24849, 44172, 115277, 16345, 29941, 42848, 14801, 8048, 26136, 36090, 41362, + 60319, 2074, 33712, 41656, 49349, 63229, 13209, 66031, 309, 4824, 48391, 36461, 47800, 73514, 39421, 155688, + 49739, 46104, 1216, 56340, 90482, 5712, 163879, 113513, 26405, 9919, 71117, 80878, 34470, 7576, 186, 167527, + 63786, 17343, 68724, 45616, 32479, 50203, 8150, 47235, 85028, 41439, 143352, 4168, 39866, 18661, 19475, 52046, + 47846, 51344, 13929, 353722, 11649, 34406, 89897, 29002, 23934, 68639, 14094, 75872, 29466, 43863, 63280, 169603, + 2816, 5244, 32027, 29855, 42864, 45790, 121470, 68468, 31828, 7242, 12594, 14488, 7410, 33485, 88169, 76478, + 74885, 61809, 68536, 70978, 49632, 26100, 42262, 112129, 47629, 15034, 77852, 1153, 111801, 32807, 15276, 117727, + 90749, 35188, 38118, 105626, 19536, 124809, 8721, 101778, 18767, 7320, 62401, 5488, 105764, 8155, 101412, 36533, + 59606, 23477, 13883, 40321, 21223, 13491, 12275, 43235, 10746, 12781, 61840, 152362, 76298, 7826, 23347, 19020, + 22220, 93982, 66332, 35455, 39408, 6329, 112746, 96397, 7190, 38758, 5458, 105620, 79654, 98403, 59395, 11902, + 64856, 56883, 35273, 53643, 11602, 20326, 70616, 82969, 82156, 35788, 123268, 58910, 272765, 24592, 15867, 1454, + 17079, 21042, 67057, 18817, 70089, 24840, 111862, 91164, 245473, 26466, 103325, 34583, 51813, 59727, 75940, 43370, + 184407, 39378, 10508, 122637, 384678, 128473, 172589, 103341, 1576, 55027, 79993, 6639, 122249, 56459, 5014, 77265, + 5064, 51717, 32582, 2149, 27481, 34880, 18933, 503, 6188, 76698, 48184, 81280, 25790, 6378, 5599, 159007, + 74361, 6010, 125775, 18286, 27541, 83541, 66715, 25065, 318284, 67687, 26494, 145603, 45430, 73737, 1093, 24588, + 31488, 141097, 46614, 41796, 620, 39230, 75054, 18365, 93579, 36160, 184470, 32372, 45723, 48418, 250572, 261817, + 192118, 22725, 77160, 79580, 22670, 4248, 83282, 74287, 51913, 89394, 15782, 18868, 4162, 31369, 195445, 114671, + 70244, 80847, 32760, 73941, 35966, 33327, 48176, 61263, 26397, 21891, 35782, 51428, 16199, 17361, 60996, 162215, + 50899, 70443, 196905, 14327, 209613, 277476, 31457, 115726, 121702, 1643, 41064, 101937, 287507, 200215, 40259, 17132, + 2993, 39858, 66709, 78788, 36101, 45516, 276535, 10475, 132229, 74041, 85837, 4489, 67345, 47555, 70268, 21923, + 33062, 17585, 37566, 31019, 76295, 41197, 33727, 44308, 118628, 54158, 74493, 8091, 78705, 83923, 8776, 31089, + 52316, 104384, 21180, 13077, 34375, 98798, 124584, 38929, 107083, 5305, 11827, 45799, 107454, 122628, 99613, 39711, + 44863, 77878, 47979, 163774, 127561, 55355, 79908, 233991, 33964, 8846, 147975, 196384, 3073, 181199, 4641, 18878, + 154010, 234469, 1978, 29642, 190914, 72852, 147040, 33070, 55967, 226887, 13739, 90555, 39074, 42255, 11101, 11143, + 6272, 2958, 5785, 149827, 31047, 148068, 44726, 20098, 5550, 34454, 68139, 117608, 41123, 74247, 21830, 126493, + 26154, 125253, 9928, 34238, 98638, 40988, 315243, 29780, 47110, 42038, 38685, 1249, 19998, 18504, 2563, 17213, + 148091, 6500, 13838, 19244, 50229, 4746, 251846, 112081, 31329, 48587, 8296, 216791, 59900, 99134, 13938, 168292, + 195442, 43920, 20408, 15133, 19106, 21571, 58002, 11833, 61347, 98426, 10306, 95246, 73497, 108255, 62936, 13502, + 70015, 18245, 80358, 41111, 682, 47734, 11486, 103861, 45850, 5615, 51099, 134183, 25776, 191909, 70530, 132159, + 38022, 64318, 63079, 172030, 148951, 284196, 101745, 31146, 6288, 10262, 10014, 172794, 37411, 22511, 4387, 112723, + 232526, 23910, 161525, 17672, 109277, 67584, 32161, 96383, 27286, 345858, 68047, 143833, 32342, 125891, 44280, 13086, + 9262, 166694, 69189, 41261, 5220, 24538, 15818, 21924, 16651, 109563, 5340, 30385, 23175, 91017, 49288, 45540, + 46740, 114503, 244673, 25970, 129438, 46907, 33785, 227986, 78614, 21905, 31585, 114441, 121925, 1940, 19917, 21156, + 66914, 81575, 3244, 30495, 88710, 29655, 12313, 83379, 127952, 105486, 99459, 88635, 5563, 32187, 8229, 94749, + 21500, 48758, 166385, 14479, 34521, 359597, 72504, 153813, 10739, 78835, 39295, 138067, 14863, 122543, 48540, 34380, + 191006, 11035, 196034, 9752, 62956, 65440, 80639, 387, 17359, 20899, 93399, 207191, 16749, 28093, 88121, 92904, + 67027, 59025, 67931, 87918, 56284, 135160, 87875, 81632, 69134, 75164, 29710, 188499, 43301, 19047, 13422, 106967, + 35039, 65093, 55023, 107550, 58883, 53155, 1578, 14587, 54466, 100984, 69351, 32950, 60823, 25977, 174836, 15869, + 404451, 6689, 11576, 4477, 75743, 45266, 31052, 16005, 59856, 29472, 81237, 29067, 86979, 42164, 23945, 46676, + 7923, 90552, 46853, 182972, 34273, 42374, 17945, 13686, 83785, 52585, 13309, 23870, 32142, 64343, 98952, 28074, + 7693, 4539, 24893, 39020, 268986, 16664, 39061, 84393, 197428, 80361, 205940, 1224, 282681, 6882, 9445, 49939, + 17049, 191596, 29434, 55100, 22346, 54975, 127831, 732, 22990, 126521, 11455, 86007, 92245, 138159, 51749, 151336, + 107180, 1069, 19546, 41449, 3357, 159316, 6574, 4724, 37104, 15238, 26063, 24160, 96724, 37317, 18138, 7223, + 49153, 51769, 152694, 7631, 7683, 64472, 8352, 2685, 31197, 127743, 6860, 92869, 43267, 85011, 42057, 23724, + 82231, 8741, 18674, 7910, 164276, 4096, 12771, 7586, 23696, 47054, 195099, 1416, 20848, 13504, 357403, 2764, + 188364, 105560, 295, 178445, 22309, 57234, 22103, 107666, 24821, 35099, 28676, 58490, 158707, 25657, 23518, 61519, + 1018, 46602, 17455, 53294, 22514, 62556, 170305, 115366, 70922, 69405, 15098, 71322, 27792, 76230, 27885, 2441, + 45589, 36981, 150699, 24146, 59709, 81228, 22766, 66205, 10765, 37617, 9373, 49056, 736, 99650, 67177, 559, + 35218, 47852, 5803, 7500, 63479, 81545, 7010, 84110, 51987, 114840, 24620, 8163, 24275, 88890, 163648, 134506, + 63588, 23081, 142828, 65953, 55361, 67896, 114542, 9127, 92929, 19906, 111372, 38827, 81964, 49480, 42737, 12268, + 4658, 112744, 27101, 301, 20122, 14673, 94899, 206599, 12330, 76979, 31622, 74309, 44058, 128517, 56436, 14073, + 13065, 23339, 21315, 103178, 311456, 16278, 14920, 198146, 72224, 420550, 41727, 777, 8337, 104777, 24184, 25793, + 211229, 26740, 119387, 100011, 38979, 100498, 23747, 45421, 22590, 8336, 22845, 14459, 138478, 53166, 57049, 20497, + 52757, 82151, 2460, 50662, 32595, 50914, 9779, 140220, 133600, 20746, 24104, 216217, 8838, 122361, 11593, 28760, + 31549, 816, 28187, 5501, 94412, 60114, 28281, 153116, 43391, 8488, 90398, 47350, 90056, 27922, 39104, 94601, + 1585, 8966, 10638, 10171, 94802, 8318, 14529, 110590, 127271, 73877, 11430, 2830, 6223, 27005, 16811, 21014, + 31889, 241922, 77341, 77320, 137038, 18139, 50332, 123737, 132910, 94235, 16743, 82586, 2165, 47123, 21947, 68249, + 57616, 1395, 50542, 129396, 230152, 209588, 78454, 147757, 6080, 219127, 4180, 9021, 10748, 81158, 64973, 29190, + 36737, 228622, 98804, 17829, 74579, 16417, 183595, 101604, 134062, 17306, 3644, 19380, 50525, 72396, 159940, 117382, + 180532, 78857, 55739, 98983, 119270, 38236, 8379, 25607, 34556, 33219, 34803, 98799, 76155, 37523, 75966, 6648, + 82394, 4084, 98676, 3845, 52595, 13580, 58240, 1922, 29258, 10438, 105425, 26130, 31435, 85783, 87939, 115936, + 87820, 77028, 181067, 59464, 67996, 9819, 19251, 40273, 26943, 18184, 84410, 39092, 183878, 10146, 8789, 33548, + 38007, 71479, 208117, 24698, 2410, 113333, 13181, 6605, 13526, 49339, 7061, 64271, 180297, 17014, 2971, 168674, + 69856, 33945, 110699, 265836, 3503, 115232, 136418, 50952, 187396, 40638, 4807, 156118, 167700, 13849, 57520, 81231, + 7838, 11640, 12170, 5741, 16701, 16659, 125534, 15317, 9199, 52795, 24781, 6825, 56267, 83437, 204926, 74158, + 3661, 59223, 14235, 194403, 37407, 20530, 23146, 12357, 65994, 11931, 56380, 259451, 23767, 79929, 18293, 110440, + 5708, 110566, 1381, 116346, 62508, 48437, 65252, 42437, 221700, 23408, 20821, 78800, 67088, 5214, 80178, 40659, + 86978, 3139, 87525, 38590, 46776, 96503, 7226, 124649, 84434, 21210, 52718, 39533, 32088, 11610, 48883, 48993, + 5612, 36169, 74879, 111083, 9149, 156582, 123119, 79206, 88244, 36781, 6276, 121833, 21685, 67708, 562, 32969, + 95151, 49905, 11821, 49025, 46750, 363738, 60238, 7126, 189612, 23817, 135205, 79928, 2979, 54100, 109851, 73077, + 506311, 12222, 150050, 90908, 2594, 81368, 57202, 25388, 3628, 28737, 8460, 86804, 40074, 10968, 92876, 5499, + 105039, 2695, 47351, 172227, 78243, 121715, 27084, 78833, 28523, 73676, 464, 68232, 6651, 130040, 127800, 48799, + 38058, 37843, 5052, 96560, 71999, 133710, 27378, 191856, 30992, 147444, 29030, 53817, 12764, 121245, 60444, 26643, + 68261, 39242, 16699, 155639, 108991, 19332, 42990, 80805, 6165, 95293, 82667, 375680, 26450, 33561, 31227, 248811, + 61961, 7643, 142037, 7514, 13400, 107606, 34976, 50694, 22426, 151745, 198926, 23162, 7490, 69785, 8890, 277275, + 60890, 30537, 37432, 49609, 109623, 3559, 109101, 157822, 2070, 19341, 18250, 88785, 12958, 30738, 47073, 37163, + 50355, 61092, 55664, 18154, 67979, 11874, 16017, 16832, 257096, 63841, 46836, 35435, 7213, 39562, 77677, 20617, + 42578, 32643, 98441, 139236, 52121, 64862, 68450, 282715, 35716, 2199, 97719, 13226, 65461, 127411, 66119, 58368, + 7516, 8148, 55990, 6956, 124758, 9239, 3153, 62014, 39268, 163536, 46944, 43855, 302, 6682, 207287, 15207, + 64712, 56673, 22223, 78977, 14977, 22415, 238137, 21853, 1467, 6198, 107406, 33222, 219452, 21709, 119024, 34391, + 2840, 1157, 34974, 22756, 34229, 50276, 12565, 13069, 11121, 120511, 69104, 16271, 21602, 41109, 62931, 15756, + 19270, 52519, 17405, 24235, 63574, 6789, 324542, 136115, 8024, 15348, 17892, 47562, 1532, 70350, 35583, 71230, + 17331, 3309, 46253, 26611, 79839, 99277, 117997, 65915, 78885, 32688, 25828, 19004, 52029, 50625, 9248, 17400, + 180767, 38886, 29357, 68385, 57957, 5909, 37897, 76460, 6069, 20372, 5141, 50706, 91265, 87494, 32650, 234722, + 61380, 65571, 34714, 45634, 55767, 26279, 65231, 106901, 8927, 283, 16073, 103627, 32881, 18500, 150143, 38519, + 287603, 17485, 853, 34227, 22149, 485770, 39484, 23090, 35029, 31381, 51798, 78528, 68876, 38737, 36453, 236345, + 6428, 12075, 1812, 27252, 199567, 13210, 14175, 2341, 46926, 622, 28321, 38887, 13412, 97447, 15960, 114377, + 104132, 9242, 11929, 173622, 21434, 107890, 50877, 49000, 366616, 878, 47215, 100194, 45060, 104282, 141046, 35203, + 110046, 219551, 85771, 84943, 81924, 108674, 74715, 12699, 128910, 32654, 6935, 167969, 45886, 48348, 61573, 81800, + 52821, 34060, 4242, 56585, 130416, 152475, 207991, 171093, 29416, 186493, 59505, 34175, 77342, 15376, 12990, 99902, + 21762, 74649, 5423, 65516, 67329, 11829, 84139, 241464, 121432, 34713, 85742, 187730, 79924, 6579, 77428, 24207, + 11724, 110158, 32973, 112280, 38625, 29086, 83056, 3907, 81006, 88966, 16041, 71498, 102033, 825, 26490, 10662, + 28338, 69696, 48093, 65072, 13326, 134496, 36471, 61179, 3250, 65892, 28533, 314299, 82056, 101706, 7567, 64574, + 82526, 61878, 9810, 151779, 38212, 40297, 107886, 9224, 21112, 83917, 6731, 127019, 12382, 20817, 46524, 7526, + 111495, 45460, 29077, 14716, 3263, 2776, 32734, 117361, 7414, 4263, 57298, 257932, 86274, 32666, 76331, 77614, + 93490, 72983, 103093, 41179, 40844, 68943, 116063, 4284, 30224, 160402, 11643, 2596, 45212, 159780, 15217, 214380, + 24019, 8607, 90193, 25716, 48411, 93174, 97695, 187108, 71367, 40950, 51935, 149531, 24941, 24881, 32250, 21110, + 76729, 22520, 11901, 44780, 57776, 164255, 34822, 2491, 3769, 55143, 92422, 73099, 38114, 63649, 64110, 240212, + 202019, 107803, 52205, 22566, 197745, 21239, 67424, 3015, 31953, 41591, 28285, 76949, 237533, 40323, 293650, 232903, + 33270, 251467, 176985, 24164, 201580, 38564, 156136, 59809, 255648, 80672, 240807, 90052, 100798, 140429, 105726, 10493, + 44741, 91259, 58405, 10701, 32241, 77032, 19646, 28622, 98468, 71458, 27207, 84089, 106931, 6037, 21906, 10904, + 10085, 71638, 18970, 12327, 15090, 155131, 570, 57108, 170358, 184285, 20866, 9713, 33154, 17127, 1501, 66684, + 66787, 23409, 12207, 87238, 18819, 102498, 86382, 527, 69760, 37855, 28336, 40134, 25061, 472, 119634, 283057, + 234005, 72393, 63914, 13795, 82660, 81969, 21503, 42354, 6295, 133186, 18259, 34816, 131975, 111080, 119914, 6227, + 16874, 28237, 109468, 13462, 9076, 139909, 173435, 140650, 4094, 59998, 72608, 46830, 25005, 51675, 154533, 146622, + 17740, 201648, 55660, 9846, 40908, 71868, 61190, 22963, 19533, 38545, 29300, 44101, 220019, 36593, 119629, 19665, + 44330, 108853, 121109, 89385, 99792, 69972, 191515, 2180, 50040, 29432, 18069, 77343, 19619, 123487, 256669, 76631, + 13950, 296596, 1597, 129830, 55228, 1167, 160849, 18579, 24423, 59175, 11879, 3471, 31253, 98945, 59597, 119156, + 95308, 79988, 122939, 9124, 103177, 84168, 28969, 42697, 184795, 16008, 50199, 163322, 28590, 6494, 60509, 135058, + 82285, 113064, 23838, 104824, 5059, 80031, 14223, 11317, 3210, 366149, 3627, 19284, 75525, 82629, 76433, 17398, + 49894, 214741, 20201, 17960, 70007, 4469, 41765, 94300, 56178, 35669, 3059, 41367, 10580, 141243, 173468, 16012, + 36051, 146008, 6174, 145965, 139681, 7800, 110797, 7035, 21617, 33212, 25669, 13652, 98736, 51362, 38127, 761, + 3555, 31131, 121667, 108117, 106306, 16338, 122989, 66956, 164189, 15339, 82154, 24542, 37352, 59255, 110432, 16682, + 63915, 228093, 103923, 44235, 47824, 168857, 93914, 68839, 24883, 16577, 41048, 298253, 145530, 10841, 15100, 232215, + 61904, 5837, 125998, 35069, 28444, 58263, 14138, 85433, 11483, 143759, 34386, 73214, 19837, 19344, 20822, 8109, + 145446, 6859, 87391, 91712, 30420, 47415, 145201, 71828, 112972, 41730, 28283, 170664, 85939, 141658, 70333, 124812, + 11835, 2977, 84882, 9672, 191233, 7890, 112346, 19182, 2262, 159541, 16980, 12043, 20705, 67775, 24464, 209857, + 58630, 270281, 312308, 672, 1753, 46565, 82263, 33826, 148334, 55096, 120377, 20727, 1197, 4386, 5122, 5934, + 144714, 56754, 767, 46661, 6887, 16011, 3279, 258372, 11223, 169694, 25814, 42211, 107667, 126684, 25371, 63630, + 60879, 20178, 24287, 89912, 77914, 7710, 134186, 56763, 4151, 13041, 161212, 270864, 57417, 45691, 139371, 26391, + 81594, 36360, 47120, 2894, 96681, 102899, 35717, 25696, 169430, 114986, 52356, 18242, 1784, 96852, 53673, 123031, + 20444, 64937, 107271, 5906, 95138, 129637, 2569, 61992, 254041, 52369, 35639, 117271, 27038, 96678, 122654, 59573, + 596, 42424, 23209, 68851, 7117, 86087, 20253, 129099, 72808, 8253, 236489, 10640, 13759, 33512, 12847, 68886, + 3353, 51042, 54954, 88292, 126776, 35156, 39154, 26608, 21074, 3070, 132841, 36168, 55322, 31705, 21862, 73120, + 27081, 96769, 100873, 33028, 36942, 66613, 15763, 33080, 39547, 359328, 23281, 74973, 139830, 177478, 3930, 86190, + 179275, 148581, 122851, 1431, 4453, 146240, 239658, 55165, 713, 274, 94886, 73822, 8722, 26916, 78701, 67472, + 71399, 84867, 279082, 235, 19204, 9012, 17044, 1382, 25785, 9114, 9013, 22506, 22794, 59383, 85470, 19980, + 23923, 137385, 187894, 268567, 104114, 23511, 100004, 3566, 11291, 14071, 28270, 6390, 25458, 111325, 4382, 14700, + 102309, 41377, 7731, 3431, 88396, 37035, 150133, 15643, 75288, 106289, 2777, 70941, 230440, 48316, 25116, 63976, + 206396, 108620, 37151, 125702, 104551, 113811, 119436, 24384, 58447, 4370, 24435, 50488, 130857, 124278, 18387, 112999, + 37247, 26953, 4538, 30899, 94734, 101716, 114630, 179272, 31548, 49963, 38658, 24697, 176529, 190718, 62623, 4144, + 226077, 300866, 53306, 58044, 65159, 50710, 63541, 128908, 20104, 14650, 142818, 6874, 10096, 32173, 44239, 137621, + 66881, 7672, 38865, 45456, 94191, 63198, 21654, 91466, 237909, 17433, 116850, 23799, 27109, 61860, 54732, 29400, + 37404, 38958, 56953, 81848, 1520, 34230, 4135, 97322, 27421, 31838, 21240, 26409, 25220, 95856, 25488, 56829, + 113003, 1614, 126, 147771, 23423, 14373, 49546, 49817, 24884, 86146, 38695, 42648, 50585, 27147, 193187, 63419, + 6286, 46605, 45100, 136759, 231877, 33670, 291180, 89716, 150800, 7898, 65327, 43541, 11789, 18785, 15127, 92917, + 3226, 15816, 97588, 148034, 90004, 14309, 143531, 120478, 60642, 53426, 39390, 100241, 5053, 47683, 6092, 593, + 202400, 56336, 48570, 70208, 61442, 84297, 267745, 16889, 132531, 63667, 41905, 51392, 175329, 104653, 24808, 36173, + 57138, 33742, 25613, 30817, 30116, 31004, 44827, 110763, 103847, 17367, 29721, 39397, 9973, 205794, 68528, 30464, + 75367, 6167, 3182, 143724, 16452, 179801, 44257, 60822, 32360, 50545, 12909, 46081, 59119, 5222, 30976, 74231, + 21246, 4141, 25122, 44442, 10191, 152872, 60307, 6528, 164804, 64131, 52788, 203594, 23305, 109174, 33076, 95817, + 61051, 86156, 81508, 7369, 37348, 36961, 59494, 6598, 154530, 185385, 273203, 32275, 13214, 173245, 225200, 147861, + 5468, 57563, 4172, 27997, 50403, 22253, 19697, 3607, 66754, 52590, 44551, 213850, 130976, 17828, 3407, 9965, + 50559, 26417, 20257, 207504, 80515, 11064, 40718, 15057, 14436, 175751, 41158, 92093, 155492, 7541, 10270, 291817, + 84017, 120763, 131324, 93378, 5472, 128009, 141787, 144291, 43107, 11112, 64353, 20597, 41240, 29285, 7429, 182466, + 2890, 9936, 4645, 26881, 90431, 118441, 79842, 3776, 70188, 15995, 35014, 25366, 382, 86180, 8302, 14503, + 76234, 35504, 66433, 25753, 48040, 723, 30764, 17878, 50211, 19521, 103260, 1405, 281038, 12735, 16639, 6710, + 237007, 94746, 1277, 43465, 32115, 22848, 2422, 33178, 142178, 8284, 101691, 76065, 1536, 28121, 15450, 56901, + 22761, 37468, 57257, 336438, 96429, 11719, 1339, 3953, 1811, 118327, 157186, 30335, 31243, 47049, 38381, 35215, + 1679, 161267, 29632, 17925, 49143, 35370, 24607, 25287, 55209, 163958, 71839, 121011, 17402, 66842, 70491, 9817, + 235054, 64483, 2945, 109216, 61494, 17696, 18951, 2128, 7462, 147844, 39181, 147057, 77030, 240256, 162500, 11568, + 34925, 71572, 23258, 33113, 87609, 57032, 31715, 36819, 78002, 84868, 113775, 145786, 9499, 100577, 142045, 35652, + 9027, 79217, 24550, 93584, 73289, 21361, 23766, 32016, 201078, 16815, 17921, 88359, 101379, 56165, 78318, 16489, + 63544, 35992, 463196, 76115, 27666, 30809, 69632, 109853, 5469, 105799, 39876, 72304, 10642, 81042, 91087, 82633, + 30029, 3451, 39557, 9601, 49816, 43559, 44570, 24502, 132979, 33107, 74019, 68885, 95620, 43778, 22107, 80168, + 58086, 115607, 53717, 44189, 351930, 66820, 12176, 349081, 116300, 90000, 19710, 15777, 2110, 12072, 7937, 100473, + 2043, 23575, 189759, 185285, 30845, 204583, 141343, 98357, 6154, 24850, 10033, 166394, 11279, 9588, 63358, 66619, + 16727, 29173, 29298, 22369, 4122, 1113, 93975, 2373, 2277, 6248, 25424, 144362, 27281, 10791, 31674, 136149, + 4971, 5091, 109071, 28111, 3650, 74833, 33069, 99452, 39060, 31553, 103088, 6083, 61970, 35073, 42159, 39447, + 65951, 82331, 17467, 274725, 39674, 192758, 99239, 74038, 75686, 221820, 29305, 145449, 38151, 141438, 74464, 8701, + 11370, 40356, 35644, 219664, 130809, 33760, 32012, 65616, 177895, 96022, 44668, 36789, 32665, 181104, 107837, 21508, + 63725, 164836, 5861, 54679, 122267, 20346, 83568, 92187, 7857, 2055, 91980, 45529, 39618, 46036, 44095, 43635, + 118483, 55547, 30683, 9026, 44792, 15349, 9572, 31258, 157755, 62006, 13108, 41088, 178624, 42632, 108286, 57576, + 136994, 75081, 20067, 213455, 24260, 59651, 156381, 28506, 41308, 51673, 109778, 35539, 22471, 31926, 60313, 141628, + 12404, 177355, 186764, 8270, 21707, 53992, 20210, 175836, 12486, 35418, 68014, 148679, 30473, 15016, 74384, 2134, + 52781, 50454, 39034, 16954, 50246, 149675, 90227, 90639, 20247, 105483, 42840, 84149, 39065, 6265, 28880, 153724, + 909, 158044, 52031, 189995, 56825, 89732, 14963, 79537, 103158, 77948, 193052, 23904, 128603, 35173, 103922, 50144, + 31542, 77257, 10193, 261793, 1089, 61599, 83679, 56827, 41935, 34672, 1669, 32964, 32744, 192677, 84032, 84980, + 12428, 221609, 53227, 16700, 37963, 17089, 18238, 394, 84420, 5956, 18576, 76244, 33134, 135230, 52741, 9872, + 72921, 31874, 99863, 233313, 208449, 55160, 197159, 30521, 42622, 223154, 80731, 30948, 168151, 65889, 42412, 23756, + 127335, 110467, 63177, 112577, 147107, 45515, 164144, 8147, 46699, 185194, 12846, 5150, 38216, 15288, 59319, 209454, + 12591, 1396, 2748, 213994, 94342, 174981, 9164, 7542, 85814, 79347, 3079, 43844, 31423, 356287, 9839, 64046, + 24944, 181828, 21425, 105878, 2605, 75931, 24468, 28548, 87542, 72786, 33573, 9795, 67473, 52048, 18016, 14242, + 192551, 248913, 95190, 112505, 4496, 31534, 647, 69179, 56321, 161887, 101346, 161387, 91819, 19636, 11691, 343909, + 17630, 27347, 151697, 2034, 6300, 29522, 1714, 19625, 256183, 30736, 41363, 146757, 114569, 40479, 15465, 2041, + 202090, 13378, 121579, 195034, 33209, 67524, 29264, 68859, 35289, 9132, 124566, 11834, 34897, 23701, 17860, 41618, + 24967, 44272, 55538, 9772, 40520, 67880, 13672, 5691, 43470, 43146, 59144, 18400, 5344, 99162, 20283, 21126, + 10199, 286754, 157014, 57352, 34810, 134947, 22482, 13869, 14283, 3260, 39498, 50188, 10381, 85601, 130984, 2037, + 10017, 115073, 41784, 35604, 62923, 26892, 47516, 14669, 49924, 117650, 194265, 354551, 23233, 13596, 123144, 1265, + 64539, 13442, 26226, 983, 13051, 82353, 130403, 88007, 35686, 34010, 54566, 1384, 19698, 66960, 132131, 70625, + 11570, 29263, 50727, 110849, 135555, 4078, 19496, 118621, 120868, 32514, 188800, 161569, 44924, 24501, 105062, 111736, + 87065, 308308, 30954, 10824, 52318, 42959, 6951, 9830, 52335, 136608, 31619, 248564, 47586, 44794, 93623, 23889, + 140906, 33780, 30924, 50467, 245885, 88351, 90491, 46859, 109834, 48432, 37672, 25466, 78668, 2856, 51536, 53156, + 9065, 1466, 166162, 149156, 46990, 132982, 13320, 58757, 25258, 298, 22182, 22431, 72022, 12639, 39287, 100922, + 61243, 26416, 20111, 29015, 40838, 101281, 1681, 96725, 4545, 9838, 11567, 53063, 146387, 341555, 34114, 20033, + 10537, 143943, 157042, 19848, 11557, 42577, 57214, 27640, 17470, 179231, 12836, 195453, 36930, 46768, 313283, 35513, + 68104, 39738, 86287, 104695, 46711, 4413, 25433, 60207, 24264, 18023, 111517, 45375, 79401, 20865, 226464, 27841, + 81043, 41593, 204624, 25039, 18225, 20244, 170119, 22971, 120488, 189962, 74489, 159216, 24746, 58887, 156006, 65825, + 84338, 9196, 33923, 25183, 81652, 80939, 67675, 13888, 28266, 18067, 6244, 68109, 13776, 69394, 105951, 38639, + 21878, 12025, 34471, 14990, 46973, 71457, 38263, 85696, 1047, 50364, 18100, 216604, 230465, 75354, 183859, 29794, + 73357, 27757, 58872, 122255, 95777, 108826, 40410, 784, 24973, 20666, 10256, 47191, 210160, 225901, 92342, 20564, + 62210, 15357, 81223, 47348, 58404, 136370, 87219, 182975, 110633, 231019, 5557, 114090, 169651, 152695, 39659, 10697, + 6937, 15420, 20820, 60557, 41870, 16729, 133108, 27320, 9909, 108465, 192359, 16498, 26822, 325219, 33762, 172522, + 191062, 29716, 26412, 2097, 76553, 124900, 73484, 69292, 27519, 32870, 80707, 31445, 96256, 2314, 70692, 4058, + 239070, 10821, 41413, 95014, 2478, 35503, 100322, 236799, 205678, 14889, 48762, 33792, 67955, 41529, 176353, 46713, + 58532, 62997, 179242, 111905, 20601, 174290, 2473, 21736, 50120, 80978, 284366, 50101, 19148, 151810, 71043, 69116, + 78501, 13969, 1032, 82510, 195724, 299148, 161084, 64084, 110740, 1411, 917, 60413, 8249, 4449, 10658, 28635, + 109665, 28104, 30492, 131970, 27446, 20499, 71921, 6814, 30568, 42498, 32084, 9024, 57631, 161122, 111788, 30728, + 31425, 149345, 39864, 222740, 49752, 100795, 1957, 8606, 32820, 154188, 210448, 11604, 65504, 95671, 26463, 47243, + 50079, 54263, 5121, 8044, 3663, 137567, 25561, 3942, 102256, 169116, 15687, 13454, 219898, 132483, 29600, 88533, + 23849, 44708, 41198, 121112, 211315, 30822, 9110, 14874, 14645, 19626, 55733, 131599, 4359, 111315, 126666, 2148, + 91767, 12358, 87695, 65691, 9528, 60012, 39959, 13807, 12449, 60771, 165784, 80519, 49366, 58389, 57808, 299274, + 7941, 94765, 85206, 7523, 49763, 49374, 51040, 4812, 107848, 65929, 26938, 37068, 8930, 3191, 21092, 30208, + 27086, 3979, 56324, 7705, 50686, 214096, 86621, 19678, 9744, 23869, 7714, 49971, 10447, 184404, 140264, 142028, + 81935, 37, 202638, 112289, 39513, 2767, 321704, 16548, 46514, 195686, 36295, 202214, 1670, 15988, 55688, 23659, + 29229, 21347, 47074, 163169, 6172, 123566, 96740, 17816, 22312, 79026, 119292, 332453, 137280, 39511, 41020, 11253, + 97759, 113084, 67597, 99824, 9806, 100148, 19488, 91425, 14445, 10529, 86640, 119945, 22976, 1450, 21578, 8642, + 56458, 34421, 23850, 768, 73391, 11534, 64803, 221561, 34983, 50337, 3048, 86930, 93760, 26610, 60674, 110754, + 174219, 46834, 94439, 84023, 52573, 9508, 44750, 79062, 33149, 17148, 39204, 179378, 59747, 33608, 33811, 72388, + 2429, 27413, 53657, 1209, 136277, 15611, 10977, 18270, 75123, 18305, 73001, 65038, 165263, 120353, 2992, 111846, + 91040, 8711, 81068, 66699, 7446, 2463, 19348, 218110, 57632, 110134, 4755, 34160, 48633, 72482, 249, 46281, + 97140, 33462, 11352, 40714, 246081, 28361, 46130, 98911, 84766, 36082, 51109, 34148, 151684, 2936, 76243, 94584, + 79918, 12929, 460, 42550, 93268, 134209, 37100, 16896, 120346, 21124, 21414, 16833, 54059, 191099, 201522, 102272, + 54875, 84073, 3895, 2436, 77858, 10986, 154654, 5409, 32996, 56761, 49453, 346111, 103590, 58996, 21227, 37368, + 45276, 61068, 74997, 6502, 11968, 190483, 2851, 4516, 19600, 140163, 119135, 18019, 25849, 122333, 26208, 1253, + 17159, 181641, 62390, 34359, 132907, 44619, 54140, 33110, 42828, 34002, 172033, 159324, 16817, 22862, 123567, 246066, + 4913, 39475, 57181, 11836, 99462, 39965, 20158, 295279, 103303, 15191, 12523, 31976, 27395, 89881, 26366, 36188, + 5737, 4209, 27937, 51814, 74184, 36752, 26910, 75407, 20749, 114757, 80471, 12921, 21160, 166449, 33748, 61876, + 14377, 111451, 28376, 51624, 77062, 4759, 31489, 8667, 131403, 35903, 220511, 203998, 158735, 57711, 23070, 54147, + 10999, 74048, 6529, 32621, 27799, 92313, 30581, 51320, 77785, 63583, 107525, 10443, 9320, 51511, 19427, 121556, + 34366, 33241, 41042, 128493, 51593, 169332, 49002, 178217, 61070, 171, 83380, 12254, 33746, 11674, 26248, 60875, + 47048, 3811, 107521, 60945, 29268, 895, 49016, 21834, 36675, 58110, 5989, 35500, 30262, 101556, 212477, 10165, + 53297, 39091, 41022, 9791, 9832, 14567, 19009, 19068, 82000, 3875, 37180, 186532, 20188, 14835, 74440, 7293, + 122292, 25795, 49957, 53535, 39917, 123027, 87789, 17677, 7331, 89007, 2300, 35386, 18160, 6491, 51684, 21618, + 68301, 13263, 285552, 298645, 185935, 298900, 126950, 20478, 134830, 182343, 19829, 12011, 15031, 116530, 76262, 15413, + 4935, 40625, 164987, 49386, 10004, 44236, 39740, 43773, 165845, 43832, 56688, 2815, 185534, 81592, 56245, 1437, + 46923, 129294, 23698, 129303, 30109, 58443, 14904, 122152, 44134, 27588, 5195, 37064, 122631, 43995, 372314, 387837, + 18874, 47379, 277, 22234, 22903, 9497, 40286, 16763, 112790, 89200, 17537, 20682, 26561, 7025, 122064, 142767, + 18549, 18358, 38049, 62248, 348902, 41526, 76877, 7321, 82871, 209789, 117544, 83895, 140345, 134154, 56621, 61740, + 255565, 75916, 191295, 230290, 135390, 60673, 50087, 3175, 63556, 59497, 24739, 20520, 103747, 5481, 54327, 78229, + 145055, 27141, 91354, 101583, 179600, 37968, 51679, 147604, 145662, 21758, 1468, 32673, 296111, 37226, 10401, 244665, + 61661, 62743, 33793, 21290, 211987, 31229, 36498, 109014, 23952, 5664, 68430, 117386, 52342, 279268, 1383, 16178, + 126343, 21917, 67779, 80496, 48450, 28456, 37591, 11298, 32919, 75914, 48144, 42628, 44277, 135351, 43365, 68058, + 82185, 31919, 36044, 33488, 9591, 46231, 87880, 19683, 62139, 164744, 13946, 67759, 205363, 64547, 24950, 14744, + 376969, 125444, 36207, 34787, 394874, 9391, 29970, 31633, 108461, 29004, 95508, 16726, 18040, 1474, 161241, 87333, + 120885, 78180, 1312, 156395, 14798, 49849, 30378, 61417, 39863, 215063, 70563, 17245, 16571, 4898, 117368, 45833, + 16794, 119877, 56493, 56667, 58271, 114337, 11790, 85404, 81025, 1115, 70207, 196483, 55206, 75037, 286099, 52410, + 14640, 28529, 108282, 19807, 118656, 48399, 13926, 14142, 6361, 12773, 19250, 37526, 44092, 14182, 3300, 24788, + 85970, 100512, 5089, 93502, 6262, 1470, 30526, 6736, 153863, 47611, 5419, 5204, 108244, 23917, 15546, 201845, + 180200, 9222, 61948, 51408, 72264, 60586, 13704, 87398, 79947, 75005, 105096, 35548, 38044, 163143, 46082, 43224, + 10050, 223780, 42559, 63853, 5735, 24066, 14942, 134623, 61221, 29913, 32948, 152876, 80712, 15291, 19415, 47687, + 5471, 16468, 19049, 875, 115689, 4926, 141440, 86953, 11391, 96224, 41116, 29097, 11661, 9977, 16554, 59410, + 184257, 4916, 59752, 123609, 20010, 44968, 127762, 3094, 60116, 31503, 22578, 77738, 30320, 46196, 21138, 9271, + 19327, 143121, 101458, 26727, 134598, 33180, 123460, 124908, 45455, 1725, 24171, 1975, 27542, 3320, 81552, 83876, + 18004, 21115, 5583, 180685, 125092, 158497, 38663, 698, 452272, 14139, 42821, 65816, 1549, 15658, 88083, 33362, + 91523, 14865, 6630, 176968, 46567, 36614, 16181, 20495, 180063, 61084, 102959, 47886, 156026, 28065, 8610, 114642, + 2608, 73306, 8419, 8283, 11174, 348806, 348428, 8950, 58848, 3040, 12266, 87926, 37788, 7990, 32289, 57688, + 65907, 28786, 408131, 92280, 80194, 123266, 156847, 70303, 30490, 3057, 70321, 174337, 5786, 5649, 71496, 65938, + 40775, 32358, 26015, 20333, 119519, 11504, 86693, 47220, 106241, 179717, 45913, 80350, 11323, 10871, 42117, 44122, + 156297, 8264, 34534, 74130, 8425, 20168, 19195, 95738, 61495, 86901, 91525, 56486, 2617, 21807, 76315, 32644, + 29675, 132700, 187527, 94065, 2425, 48583, 146946, 19438, 59886, 21696, 112525, 185026, 112582, 107474, 21779, 520, + 49142, 27517, 27611, 26275, 59618, 68585, 34382, 269197, 4863, 78549, 51824, 198549, 50597, 91695, 79132, 75817, + 86710, 86822, 13732, 13511, 50650, 3411, 18621, 279970, 168632, 42104, 4038, 32572, 27693, 8881, 65349, 54005, + 85641, 7547, 8478, 41579, 83643, 30439, 9416, 5869, 18993, 49065, 16745, 12818, 25768, 25667, 58681, 44991, + 84284, 204061, 61201, 50637, 28090, 5082, 34635, 107900, 93592, 201432, 170018, 36616, 36627, 200119, 6933, 53709, + 312804, 36724, 17312, 3158, 43381, 5167, 26919, 23400, 9887, 26673, 28631, 66018, 9402, 230847, 24255, 102572, + 100931, 154504, 170013, 201115, 97165, 34404, 357, 179763, 3311, 108, 53023, 171976, 173330, 95887, 211961, 23099, + 66805, 113640, 18352, 46361, 28935, 107138, 46668, 51711, 4963, 45839, 54816, 7932, 184460, 1611, 103983, 141455, + 3201, 270150, 175156, 43537, 78102, 12443, 37971, 414715, 19126, 6340, 130684, 52220, 21607, 284399, 102147, 95375, + 37496, 136001, 219663, 16689, 24938, 7390, 85416, 109375, 22615, 13046, 57826, 15407, 16153, 137199, 62614, 211231, + 32862, 32298, 16119, 175170, 134792, 46807, 3951, 27849, 153318, 105648, 120022, 22136, 61120, 56637, 91935, 136213, + 6067, 4114, 134337, 42278, 2812, 85502, 6569, 159359, 12826, 125922, 25619, 62295, 12792, 79563, 100822, 11222, + 23825, 49867, 29919, 872, 37559, 4587, 112918, 136034, 64662, 126530, 131167, 14498, 202250, 56243, 23092, 121186, + 102694, 22868, 89387, 64332, 155488, 39744, 156087, 176575, 85881, 39781, 47236, 12249, 149193, 26406, 1951, 155992, + 46233, 184401, 41366, 8036, 65383, 139356, 189733, 213982, 15521, 49577, 159577, 2521, 106982, 58186, 55467, 51702, + 11358, 8682, 17785, 29934, 176786, 109645, 122828, 41281, 25752, 29762, 94798, 25186, 39717, 17547, 52095, 8022, + 34208, 129165, 49581, 119910, 24510, 12967, 247959, 15952, 32464, 81364, 137598, 14637, 77742, 123065, 16222, 61255, + 39371, 43724, 57019, 24082, 72028, 104622, 27929, 89643, 138229, 10055, 122728, 41443, 60688, 136821, 3274, 7812, + 71386, 67606, 3295, 100611, 102834, 3428, 102932, 35148, 132477, 142278, 25402, 33288, 2208, 132017, 146591, 21568, + 11548, 73095, 756, 36537, 63670, 246597, 20653, 141984, 271279, 12711, 23553, 27463, 28351, 23214, 77413, 75614, + 30338, 23444, 235758, 28565, 38620, 46299, 28150, 5788, 32491, 43962, 168549, 29503, 99845, 200267, 70204, 12986, + 143885, 941, 50969, 55284, 152266, 187576, 3532, 57733, 13252, 143761, 54421, 60086, 2825, 16104, 18211, 69263, + 178663, 103869, 8702, 98648, 108097, 15531, 162361, 61008, 1775, 84427, 119944, 23016, 78201, 82106, 24005, 236154, + 14897, 34582, 63819, 36233, 113573, 102759, 41120, 42814, 163346, 29508, 164161, 54185, 62292, 9036, 39296, 6640, + 171129, 26814, 9984, 294489, 22183, 40191, 69406, 90599, 96598, 18066, 85600, 87824, 38733, 247243, 7387, 161131, + 64971, 38204, 92974, 85988, 166776, 29624, 69098, 72876, 117445, 68760, 22363, 52379, 9968, 148571, 26622, 186006, + 146393, 31071, 60120, 13612, 44677, 19448, 68338, 7874, 74867, 64490, 29421, 124401, 20908, 16585, 60838, 7384, + 97328, 50410, 20319, 19156, 12761, 5816, 351477, 2786, 25656, 165861, 173732, 7123, 26785, 149182, 20793, 27143, + 9148, 25949, 18006, 7795, 112344, 192537, 14513, 5682, 26115, 40202, 70306, 48084, 99176, 7153, 108473, 44332, + 110121, 9428, 8180, 27860, 22437, 60581, 205918, 99858, 49547, 116936, 35145, 8901, 6180, 5175, 18803, 16208, + 79320, 28649, 38726, 54774, 5835, 30655, 86440, 25966, 31392, 70939, 20843, 111672, 43328, 94380, 88995, 15321, + 33377, 2984, 16233, 43332, 75870, 19044, 156499, 127031, 119860, 122514, 65060, 17517, 69497, 64192, 44374, 6455, + 80273, 40630, 92629, 46425, 7325, 37171, 79359, 140226, 155219, 32133, 40753, 22059, 43167, 16881, 38630, 50086, + 111173, 44484, 5941, 101815, 28347, 373200, 356553, 74832, 20222, 177340, 108041, 180076, 3763, 27194, 171523, 10928, + 71752, 148205, 39888, 74776, 55041, 6358, 148477, 294704, 47252, 52974, 89175, 4682, 14618, 39392, 58585, 50619, + 28088, 100276, 36110, 31596, 15012, 93646, 4953, 6629, 97805, 46485, 18765, 9249, 194698, 52307, 85352, 33772, + 54636, 209534, 25378, 210, 2036, 151416, 255499, 40976, 41349, 134520, 150432, 11157, 6173, 62536, 56094, 34646, + 96604, 85745, 27243, 6622, 61530, 18737, 8699, 115599, 51859, 41290, 143452, 40201, 43782, 14061, 74656, 29726, + 13361, 136026, 23149, 4742, 24334, 18683, 24925, 85168, 22668, 224320, 16450, 76194, 24792, 35366, 2119, 56976, + 7070, 16021, 24155, 51463, 23441, 3622, 46105, 126702, 16789, 276646, 86393, 21589, 3209, 111511, 12134, 13169, + 36211, 227939, 16170, 16148, 208475, 99712, 10351, 419718, 26242, 79670, 44112, 4569, 32880, 339579, 30648, 97335, + 122181, 70716, 13387, 85134, 182407, 26932, 273051, 12495, 21444, 28811, 92202, 62532, 31060, 48235, 131669, 47086, + 88459, 54224, 9712, 80410, 29929, 35255, 5466, 174314, 77907, 10011, 1541, 31577, 12716, 153148, 263135, 100373, + 10934, 79991, 36589, 35238, 97005, 23398, 171539, 8580, 20599, 128843, 2723, 53858, 31690, 44768, 11242, 19051, + 8403, 12581, 30429, 200097, 58445, 64447, 17535, 77337, 30303, 7870, 87634, 120825, 22700, 347, 32033, 8588, + 10336, 47933, 95540, 18314, 86731, 43480, 122415, 48270, 103115, 61655, 3386, 11664, 337709, 27487, 202769, 59646, + 72556, 173070, 8469, 81115, 46788, 18684, 133404, 96594, 112566, 86812, 64663, 85306, 47684, 65109, 21999, 787, + 67089, 42642, 9467, 104964, 53548, 77932, 89601, 50531, 36874, 103369, 55381, 55386, 56487, 110574, 42321, 104493, + 41387, 87478, 10390, 37145, 125985, 271334, 8940, 28636, 26893, 223188, 56969, 202288, 40071, 34929, 116962, 28213, + 106683, 28231, 42013, 24525, 73712, 66578, 95581, 82535, 18787, 2469, 64097, 192370, 40105, 33082, 137529, 8248, + 72992, 28876, 9399, 7582, 67246, 4830, 5079, 188992, 137276, 8793, 145644, 146116, 50802, 69878, 200577, 15640, + 36790, 214109, 63824, 55941, 70328, 47477, 2167, 52953, 138827, 24284, 37429, 16033, 22466, 117793, 20960, 43422, + 39263, 48912, 81141, 192574, 183295, 1837, 14713, 28579, 29858, 381, 62358, 27575, 50975, 47277, 158226, 20747, + 9322, 21799, 8759, 19551, 57397, 25924, 60257, 23908, 10654, 24152, 18912, 12247, 24364, 134961, 117953, 43806, + 30383, 28739, 36894, 57851, 55799, 12140, 208514, 12059, 41600, 16395, 47450, 48286, 23584, 118890, 25589, 8681, + 127295, 234074, 47071, 125810, 296610, 11331, 18254, 15170, 129078, 16080, 226323, 40895, 143558, 94050, 23705, 131198, + 244131, 60925, 25356, 21260, 86397, 300199, 46792, 88237, 36049, 206902, 15590, 21351, 1085, 93619, 11791, 83320, + 80677, 34168, 26403, 64840, 3820, 15926, 1847, 16734, 108139, 3510, 11982, 209610, 5476, 22002, 108428, 55260, + 34767, 29252, 98069, 88530, 24683, 25427, 54524, 21159, 7758, 58183, 73508, 29449, 13060, 45920, 148846, 105330, + 7239, 2883, 81088, 12697, 131671, 7549, 43047, 4805, 250593, 12157, 34279, 12914, 59556, 76223, 25084, 20506, + 103392, 43609, 100817, 171460, 29810, 37880, 81256, 174784, 4188, 149828, 64134, 59705, 252323, 25997, 44940, 97369, + 39404, 5069, 90268, 85619, 116877, 19634, 56035, 36905, 7651, 33380, 130707, 133829, 43600, 25142, 75703, 40295, + 40338, 60316, 24687, 44342, 13554, 8678, 9961, 1732, 157253, 93469, 29687, 49688, 39196, 21767, 41224, 21529, + 25978, 36956, 66355, 33481, 144387, 50146, 129773, 13311, 61211, 62169, 77703, 101581, 234, 27986, 9318, 13341, + 50104, 58984, 15733, 9924, 6129, 59308, 32036, 85687, 10449, 47782, 8891, 25720, 93777, 35277, 8953, 13811, + 9240, 22192, 6432, 17202, 356378, 52015, 66393, 12508, 274148, 255059, 67115, 30737, 4439, 11480, 231776, 166051, + 72970, 82790, 96236, 26126, 3724, 86291, 14281, 11950, 147770, 105431, 20726, 77543, 78680, 17490, 13496, 21992, + 62570, 4476, 98692, 112842, 115877, 74277, 124883, 83834, 40027, 21132, 19464, 47232, 40547, 89457, 28687, 14573, + 36817, 103723, 300665, 15319, 224392, 26400, 40495, 22877, 64609, 18201, 23154, 72374, 34795, 27583, 78778, 23667, + 165027, 32508, 73622, 56731, 67440, 2495, 103298, 105353, 2477, 41716, 11030, 8686, 37206, 79590, 125885, 13625, + 23431, 4395, 220465, 150736, 50754, 5523, 27215, 232561, 164797, 91433, 63055, 7083, 46018, 251531, 40722, 70383, + 94995, 7924, 77757, 28613, 170982, 867, 21717, 13321, 27051, 21566, 114874, 18681, 7957, 7438, 19655, 84979, + 22767, 101166, 277403, 47583, 3674, 112331, 65307, 4882, 27900, 40861, 34152, 26594, 56419, 29707, 25132, 78891, + 18930, 58166, 23382, 32025, 60701, 65952, 21108, 607, 41302, 44913, 98469, 73043, 2692, 100592, 76874, 140991, + 84749, 15560, 29248, 219368, 339721, 48121, 96609, 79943, 61996, 45630, 28536, 16244, 111094, 26428, 57889, 25111, + 80221, 69552, 27326, 124506, 50129, 75574, 64173, 83505, 1045, 142814, 170324, 19671, 8153, 208336, 12576, 12623, + 62945, 184743, 32415, 73714, 19202, 2698, 136438, 116392, 8250, 15337, 70178, 157991, 37208, 8242, 26035, 58589, + 37418, 6014, 58480, 1274, 32560, 127652, 47847, 148702, 79477, 92504, 29034, 87904, 41106, 61295, 72948, 79082, + 88569, 164147, 34772, 53574, 33963, 129292, 2501, 7461, 36693, 68888, 18880, 65806, 5892, 424331, 17516, 24390, + 30570, 2113, 9490, 25280, 1581, 110856, 24330, 24537, 66471, 15890, 104155, 126634, 49647, 82695, 115436, 114480, + 11922, 54150, 34729, 37512, 160717, 69615, 2014, 40558, 29442, 49537, 9489, 90588, 5643, 197221, 9955, 87575, + 114865, 94728, 2057, 19542, 82962, 71746, 2865, 8021, 95982, 61016, 32535, 150782, 132098, 15408, 30334, 114765, + 22633, 27477, 74001, 34329, 22838, 9812, 99985, 6414, 94726, 41615, 168290, 212638, 54556, 24532, 127124, 32488, + 28566, 631, 37608, 9436, 205039, 166709, 41813, 62681, 162340, 51007, 104187, 135517, 33216, 370029, 46677, 42823, + 16849, 22305, 32170, 4155, 35847, 216420, 4908, 9704, 221339, 1461, 36764, 69322, 94851, 163847, 168141, 1238, + 26533, 64284, 196577, 46554, 71469, 97500, 18030, 75035, 1805, 59036, 59485, 22807, 3804, 35946, 47500, 82026, + 12935, 1196, 59186, 36123, 45483, 48905, 122736, 84743, 71020, 21859, 105891, 19409, 36310, 14933, 324632, 9477, + 65381, 15301, 17544, 116221, 192960, 28345, 33914, 9479, 34240, 10843, 107872, 10760, 35165, 170015, 15849, 66429, + 59773, 117561, 48895, 53810, 1248, 297457, 78131, 22215, 46954, 15473, 66440, 19176, 155332, 67372, 63874, 27562, + 96864, 44731, 33316, 68027, 4246, 61528, 60417, 153158, 388800, 242889, 139912, 30680, 16129, 184234, 14284, 220334, + 57133, 2684, 29537, 163409, 74592, 22341, 14608, 7820, 44807, 52082, 34669, 735, 442014, 5199, 156652, 115585, + 38203, 46928, 26751, 41163, 42574, 23000, 40485, 193202, 80818, 24685, 30063, 46336, 91592, 38350, 16019, 204886, + 26377, 35729, 24114, 14839, 36424, 82137, 58468, 208317, 65760, 107517, 111976, 169534, 977, 88148, 88506, 104279, + 77387, 49741, 55001, 102462, 22628, 16787, 70803, 44152, 147610, 49926, 3305, 34988, 28018, 39850, 1762, 172940, + 30561, 35822, 23734, 154547, 98454, 6287, 35558, 37540, 6969, 16948, 182134, 68275, 119628, 26385, 69050, 27987, + 63648, 91787, 15241, 69688, 18170, 23405, 63549, 172789, 36854, 149944, 199582, 50387, 26601, 61662, 165764, 15740, + 64018, 188861, 204663, 14506, 22027, 34003, 37949, 76827, 37279, 69128, 41728, 50954, 51395, 91070, 77327, 418272, + 152934, 102026, 34299, 2147, 21153, 7074, 4236, 29765, 9430, 213559, 43803, 10595, 58760, 69911, 261653, 87745, + 194742, 224, 344942, 28518, 5330, 188455, 29445, 39380, 55115, 37739, 135330, 40527, 34158, 67980, 2019, 20921, + 28917, 61353, 29277, 143760, 174111, 25315, 233758, 5380, 13171, 38385, 49725, 63589, 122326, 12646, 695, 115120, + 1526, 21427, 111543, 128260, 43896, 37771, 92334, 46393, 66094, 257494, 18659, 47526, 25325, 7287, 24994, 1200, + 4234, 136264, 35864, 150235, 148354, 9687, 28790, 43378, 11450, 185999, 16029, 3010, 275, 54840, 18620, 5465, + 18999, 14941, 133430, 7102, 112191, 67383, 37978, 43984, 44365, 118346, 5294, 4294, 22723, 21609, 95097, 56653, + 68409, 57456, 462, 22817, 8733, 4115, 95791, 28344, 57746, 79153, 1397, 207599, 96565, 211156, 90894, 88357, + 75007, 67110, 7600, 143795, 14196, 17993, 7370, 47401, 108844, 40816, 2129, 578, 29475, 1352, 164155, 115861, + 88599, 265011, 72917, 44900, 177563, 66133, 61076, 81186, 100792, 66415, 27198, 24480, 106156, 32719, 8226, 19302, + 86323, 65704, 91571, 74710, 93726, 40774, 166720, 71206, 14248, 159482, 104866, 13711, 135341, 12882, 22933, 26886, + 194131, 14226, 57391, 10865, 40126, 114370, 59004, 62802, 47099, 37870, 61471, 61712, 14779, 23150, 60956, 17913, + 8272, 31742, 43627, 355564, 39597, 43789, 24868, 17215, 95983, 10850, 171578, 95826, 171398, 43329, 52382, 39205, + 65882, 22816, 30560, 5482, 28052, 257734, 116033, 49489, 10393, 42391, 140158, 74221, 47213, 10652, 133629, 42182, + 40689, 31081, 114221, 24833, 22120, 71238, 95884, 1589, 72212, 29981, 49555, 59882, 106829, 22147, 12985, 19337, + 34964, 98868, 7993, 32641, 109146, 79730, 82886, 12040, 753, 26623, 20550, 6160, 648, 37626, 156500, 74280, + 21660, 112069, 102650, 29846, 30047, 37920, 38707, 22416, 17527, 43165, 20567, 16777, 181025, 163230, 77041, 93275, + 5619, 104536, 14442, 35376, 145357, 711, 91293, 267964, 4085, 61569, 2548, 27538, 216883, 57568, 2256, 60727, + 9359, 183328, 33919, 77609, 186951, 2807, 102609, 77085, 24779, 43042, 7697, 75938, 53931, 15103, 12294, 16402, + 24545, 7447, 558, 128098, 36197, 23062, 46707, 17077, 223296, 66504, 27982, 13141, 62628, 55296, 54207, 95832, + 168101, 45485, 3146, 80848, 4243, 2868, 16953, 33358, 107313, 27188, 53837, 35648, 30321, 5098, 8014, 6864, + 26642, 216648, 15542, 69518, 13049, 164033, 260760, 92714, 51059, 3646, 38963, 53961, 31027, 65718, 8441, 195966, + 107912, 99428, 28516, 18239, 807, 22455, 35712, 38565, 73550, 133659, 118825, 32367, 26551, 30301, 28367, 11762, + 84369, 86004, 37814, 44184, 122422, 57026, 4072, 40865, 165872, 25809, 54060, 35341, 49754, 17581, 103700, 118548, + 74213, 178685, 2053, 49373, 234264, 17223, 35164, 99392, 33151, 130808, 2338, 24598, 52014, 213186, 156444, 16006, + 33100, 87907, 2116, 183683, 87183, 121897, 25550, 4995, 22365, 1221, 128172, 24344, 52500, 5554, 89782, 2016, + 40013, 70221, 66896, 49588, 23302, 76353, 61452, 21765, 5652, 195585, 31863, 21028, 72723, 127694, 101106, 10744, + 21404, 46840, 4864, 158406, 26107, 219205, 22949, 68216, 48434, 46124, 8871, 21467, 587, 110874, 46178, 110709, + 94049, 110687, 194252, 73380, 168493, 40871, 84591, 4279, 96418, 78366, 113568, 80733, 32871, 103415, 9257, 39835, + 70860, 74701, 43788, 318366, 31709, 7528, 5382, 22765, 25128, 2525, 52257, 20911, 443, 56027, 55517, 12403, + 71597, 15617, 196829, 263547, 166253, 113889, 151910, 27229, 15670, 56695, 27118, 39691, 70994, 5859, 9227, 98270, + 26341, 176547, 121365, 69700, 133675, 164631, 110015, 41477, 28280, 6013, 20657, 210461, 75491, 126050, 3380, 248649, + 54756, 7374, 93034, 88191, 47955, 22642, 141231, 44151, 56028, 10051, 42742, 54389, 26182, 179888, 17595, 29573, + 11952, 45860, 19676, 264, 113272, 40733, 10627, 68835, 472197, 159006, 64051, 34066, 64640, 150352, 25206, 68762, + 110753, 23633, 22112, 20042, 17919, 163270, 55949, 19557, 337, 124601, 60155, 130764, 50642, 63188, 8357, 143714, + 22576, 35676, 36362, 24812, 142, 40272, 89438, 24448, 87371, 105259, 16947, 36015, 53391, 136, 23199, 36073, + 93210, 18431, 41934, 10593, 126694, 48847, 17533, 34029, 15285, 65070, 33592, 113537, 19642, 73194, 284546, 137425, + 85667, 129350, 26968, 6511, 14148, 31670, 30205, 11076, 1506, 220046, 7034, 329567, 42092, 19955, 30867, 21935, + 52962, 31820, 69932, 14236, 33243, 12574, 10827, 39690, 11970, 174135, 28153, 146891, 20734, 126712, 21323, 40265, + 135843, 199355, 58115, 49038, 57044, 54396, 16409, 31133, 58880, 32408, 26984, 27298, 13002, 35976, 171154, 61387, + 219134, 82951, 17625, 78088, 22876, 4261, 5497, 9679, 64754, 2492, 263964, 159492, 232519, 20547, 74430, 377501, + 4257, 30374, 461, 33465, 43120, 162710, 65294, 50518, 321573, 5955, 131205, 93895, 24799, 9658, 5858, 13871, + 64526, 19652, 2901, 70075, 124728, 146097, 98082, 100551, 52579, 153823, 113631, 22528, 81472, 25792, 60475, 32767, + 70831, 74749, 72861, 41755, 276848, 7157, 25389, 110028, 17403, 27510, 251623, 58039, 74359, 18091, 50708, 89467, + 23021, 20850, 106672, 3962, 182101, 38811, 122104, 32394, 74597, 3381, 18651, 101115, 23744, 97817, 110293, 37253, + 148267, 36198, 3519, 55601, 12055, 42944, 34271, 133263, 7976, 95413, 91635, 5598, 5349, 58679, 57950, 154564, + 11772, 149366, 22890, 8626, 67540, 3799, 18687, 5012, 167347, 116945, 44732, 33287, 65318, 71035, 71417, 65941, + 18720, 30185, 48199, 202749, 127832, 1587, 3246, 105625, 108238, 117159, 28014, 104782, 22828, 55738, 81738, 11163, + 90233, 17250, 4750, 188510, 9987, 20638, 16031, 14461, 259080, 49305, 190390, 14691, 118185, 22004, 72005, 37574, + 73209, 86700, 86886, 144339, 79270, 53654, 14505, 129204, 13775, 25931, 7131, 15330, 90100, 52622, 10877, 9834, + 137742, 2258, 125407, 10878, 90799, 219424, 85936, 9073, 70569, 42871, 11487, 17205, 15699, 176689, 145652, 13840, + 19961, 3187, 108771, 19907, 9087, 61130, 38770, 130505, 67475, 29769, 18202, 59283, 57872, 91097, 48087, 6535, + 39731, 194914, 82462, 72799, 8810, 20369, 69590, 161138, 134897, 1655, 228453, 124246, 131868, 32125, 24266, 32982, + 146849, 36964, 41316, 2497, 19898, 151088, 186502, 25460, 3334, 5126, 116461, 28025, 2281, 35537, 44808, 12910, + 167061, 87358, 186367, 9640, 91073, 100691, 122545, 45892, 60356, 46145, 149118, 90693, 467742, 151743, 138009, 53160, + 74712, 7571, 311165, 28817, 188, 2377, 69313, 29598, 53179, 50217, 129275, 459470, 137679, 65308, 18477, 51126, + 92769, 135453, 6508, 23061, 29241, 3159, 63876, 36946, 9537, 41270, 4306, 50909, 132595, 108954, 42730, 19664, + 80119, 116744, 47895, 141726, 1041, 20154, 230804, 208216, 88962, 27133, 7095, 34643, 5976, 24608, 31719, 12859, + 40171, 32072, 88409, 11313, 44911, 91010, 24472, 25842, 102859, 76732, 14848, 25181, 139059, 102051, 109383, 112500, + 104558, 41471, 92391, 7314, 98987, 4288, 8166, 373942, 47761, 86881, 41222, 3712, 19272, 32290, 21185, 46273, + 71472, 64765, 57109, 135608, 113864, 48751, 60216, 15724, 175377, 27567, 112796, 18273, 73338, 51592, 245687, 100900, + 10857, 55010, 31720, 50806, 23402, 71732, 1067, 129268, 23758, 39673, 37515, 71954, 1591, 31733, 27822, 35424, + 139864, 46227, 8017, 50399, 5644, 74021, 28076, 85584, 4076, 127136, 37464, 102260, 118760, 24879, 11109, 28407, + 16427, 136669, 88300, 8782, 134198, 49502, 5217, 243528, 18853, 31871, 85719, 9313, 20291, 111347, 8859, 232793, + 100849, 152830, 105869, 31365, 37423, 46174, 5288, 386390, 22038, 11906, 5770, 10139, 36677, 146746, 11499, 81033, + 19071, 79322, 71912, 26542, 195521, 10379, 25001, 31339, 57445, 2750, 6263, 58554, 11069, 15459, 87289, 89651, + 31869, 529, 435067, 57142, 55718, 10724, 15530, 50660, 66882, 102319, 10462, 143371, 148490, 217411, 10252, 44760, + 44, 24999, 38673, 7037, 41296, 7080, 103758, 32647, 75242, 101191, 76455, 28603, 49704, 54470, 60157, 16716, + 166810, 63760, 52521, 49375, 9906, 135040, 4540, 38220, 20943, 103257, 50219, 154213, 122258, 73230, 121482, 29653, + 49112, 43518, 71058, 63793, 105667, 44288, 91681, 25034, 15969, 72202, 23056, 81246, 10344, 93662, 911, 44819, + 6408, 52562, 5953, 16497, 187694, 1131, 63578, 47977, 21399, 61396, 149533, 57971, 72742, 5611, 13207, 221621, + 58970, 79728, 27059, 171306, 14867, 7768, 72956, 52885, 14376, 30921, 9280, 8158, 81889, 33376, 39155, 71120, + 41856, 131166, 105010, 30693, 23225, 58453, 31791, 3690, 15042, 11816, 43900, 117272, 56993, 64225, 105202, 54205, + 16074, 71491, 22908, 25691, 131389, 59527, 11646, 42043, 74276, 34270, 28537, 26163, 72407, 41791, 219412, 115141, + 53875, 38258, 76312, 84827, 383108, 40765, 11202, 132777, 53597, 167648, 158387, 30192, 37363, 40773, 1146, 72250, + 68993, 32806, 59487, 128806, 44854, 6158, 96774, 39870, 122548, 5997, 50579, 45575, 430927, 3282, 94975, 100509, + 198279, 200501, 32872, 18750, 38430, 107646, 104178, 8936, 80409, 100084, 192864, 160858, 12245, 67627, 83578, 90755, + 2981, 54132, 8983, 55539, 628, 33889, 292355, 74386, 2818, 172034, 214107, 19271, 17760, 834, 34352, 169686, + 37437, 145284, 73189, 54617, 238229, 4948, 71657, 94135, 7968, 30905, 23213, 304209, 46892, 61572, 104947, 117623, + 2200, 107472, 204184, 88505, 3730, 10311, 20501, 150712, 34190, 70936, 38672, 37360, 65983, 2571, 19225, 134448, + 37959, 151534, 4637, 20807, 112291, 80673, 5980, 35705, 87850, 61874, 47965, 31875, 70827, 182878, 35126, 164376, + 6522, 43040, 97902, 71998, 20750, 38267, 227637, 76251, 73913, 116387, 6656, 46715, 111621, 13498, 6684, 248541, + 41652, 32337, 11354, 14685, 19587, 16826, 46225, 7338, 2780, 13317, 78374, 44055, 58668, 1088, 55437, 86278, + 25916, 40461, 78119, 116159, 85259, 146157, 14297, 2255, 18200, 39030, 6734, 42798, 168962, 119973, 38038, 22310, + 95781, 125342, 77701, 24562, 42445, 51140, 84077, 79797, 102050, 55904, 96004, 87841, 7776, 193841, 23065, 73430, + 57662, 41418, 52176, 99003, 103313, 45282, 85005, 26027, 47742, 65487, 4547, 20906, 96358, 53110, 36530, 18902, + 41964, 66288, 159127, 54902, 66174, 22317, 184026, 50221, 100396, 167624, 175279, 22979, 29069, 146485, 480, 42086, + 204735, 209840, 27750, 486, 19679, 2874, 101071, 35317, 27978, 40196, 245641, 90054, 7479, 4028, 52060, 61770, + 40264, 884, 39262, 47984, 22534, 2991, 111271, 48232, 61183, 27973, 80376, 56009, 36081, 165143, 131566, 47390, + 107436, 17924, 139245, 18948, 58223, 25723, 60245, 119466, 14680, 77779, 96719, 17119, 23002, 182396, 70393, 3161, + 101311, 78419, 54640, 23621, 24716, 19555, 111633, 409, 124108, 27386, 2245, 258216, 12908, 23526, 10212, 69858, + 5646, 124783, 685, 22255, 31750, 16915, 43000, 28583, 40380, 54912, 117699, 74670, 14215, 28794, 179610, 6571, + 232799, 21298, 4374, 40448, 102772, 82421, 69208, 18356, 14122, 42989, 68207, 134104, 96775, 342439, 22630, 77974, + 61398, 44393, 92954, 93632, 50917, 174764, 71894, 24307, 12096, 8836, 209934, 117370, 149880, 80277, 3249, 24377, + 67833, 1203, 220478, 45163, 598749, 19930, 49756, 19462, 124194, 104517, 87257, 36828, 155871, 228667, 44446, 48887, + 49216, 262088, 45585, 33699, 790, 142940, 1214, 51939, 14677, 49283, 82736, 30811, 65319, 13215, 29155, 8388, + 56917, 46741, 80552, 21318, 7440, 61972, 13877, 113912, 145744, 1135, 64582, 45451, 95701, 94255, 93908, 15993, + 12206, 4906, 63468, 345616, 49405, 22021, 38717, 72500, 129269, 41952, 6995, 118167, 76199, 30271, 54602, 17741, + 45732, 43176, 108447, 54078, 9767, 2976, 31141, 98669, 11058, 3418, 36920, 28316, 9047, 12901, 81784, 44333, + 210885, 31029, 19486, 48518, 11051, 76288, 43203, 81787, 7392, 48679, 35182, 66212, 26307, 52539, 2090, 49456, + 2130, 146238, 10565, 420461, 8132, 25081, 22634, 263972, 147526, 30634, 102378, 79556, 20802, 111069, 161896, 208679, + 232698, 76981, 47548, 35236, 115660, 54019, 196404, 365534, 50060, 190162, 23999, 64445, 59789, 24002, 10716, 4605, + 57344, 32663, 9089, 10394, 107623, 22597, 164592, 49576, 80343, 5393, 4211, 9135, 112676, 3507, 56842, 26209, + 23291, 13451, 13815, 72724, 9866, 28546, 4585, 11238, 160971, 144491, 17045, 4629, 34032, 57499, 26471, 40064, + 118291, 51528, 233736, 42900, 15719, 86514, 25147, 31587, 59730, 27331, 34546, 93487, 164911, 55981, 1017, 52398, + 28975, 111773, 14576, 7163, 2659, 23748, 94127, 7591, 58046, 118031, 293274, 2302, 78480, 108856, 26363, 33313, + 21854, 78915, 9334, 18530, 66209, 195667, 8136, 17789, 53863, 87424, 64346, 25683, 109085, 21080, 41596, 10885, + 116045, 55411, 15616, 32700, 29021, 102829, 16981, 4307, 46481, 113835, 166601, 106369, 107552, 19257, 124248, 5139, + 22130, 56921, 41150, 3696, 18764, 5694, 250839, 81058, 70254, 74576, 7048, 33695, 31272, 9587, 51251, 12329, + 11300, 219240, 77500, 229429, 52460, 62159, 41229, 11359, 43933, 8689, 31439, 145750, 84738, 78427, 136685, 118384, + 20721, 79633, 31973, 11098, 53869, 52988, 3196, 72237, 190840, 185112, 79139, 28639, 79673, 97047, 9398, 15414, + 105300, 137313, 85877, 40993, 7561, 3929, 57151, 108868, 321817, 76740, 180609, 35166, 66924, 90464, 82436, 5838, + 13940, 45290, 67026, 19641, 33281, 26857, 36996, 98291, 101046, 54893, 50303, 114759, 183181, 305580, 14099, 33514, + 32176, 17094, 121586, 67669, 71878, 42975, 11177, 90273, 5678, 12886, 12867, 27433, 62924, 67753, 13445, 21183, + 79535, 24720, 155338, 122583, 56646, 51283, 120295, 43762, 40303, 86530, 14604, 190243, 19559, 88421, 25961, 150460, + 27703, 51292, 26294, 109748, 93042, 39631, 12236, 40405, 73368, 57016, 74750, 36429, 42187, 74845, 176283, 98118, + 3670, 19744, 24913, 39170, 37376, 11919, 150215, 7637, 46440, 61291, 85017, 262212, 7023, 214701, 15499, 13794, + 36816, 64790, 2778, 127954, 109628, 14013, 60615, 35098, 20680, 32322, 6631, 27129, 5940, 3801, 64451, 3605, + 276440, 16166, 42526, 46620, 275233, 3268, 98778, 42589, 170848, 13985, 52719, 40320, 112093, 58444, 41626, 122502, + 136996, 26123, 73872, 45093, 14984, 31647, 91921, 180982, 20226, 196060, 19058, 85542, 111441, 139969, 139431, 39188, + 77693, 106755, 134828, 38509, 112960, 28035, 81045, 32920, 48577, 41211, 1169, 51509, 39370, 57152, 7562, 65585, + 55707, 16633, 123632, 42194, 50314, 21232, 7589, 10730, 123404, 177035, 1102, 100263, 26570, 53756, 25965, 84962, + 54281, 98616, 1761, 302765, 61372, 4231, 11004, 6580, 123391, 152361, 53908, 15894, 4857, 138528, 185271, 79781, + 35928, 183068, 199461, 71306, 246740, 25571, 212575, 29887, 132507, 135536, 39243, 171966, 106646, 38340, 64208, 46548, + 44241, 12489, 59841, 52248, 7196, 49745, 18871, 214620, 92258, 92083, 128377, 10678, 9825, 273, 124186, 8170, + 37688, 47121, 81217, 46232, 51197, 22560, 24038, 76445, 303141, 269387, 168244, 7423, 5590, 148765, 8018, 44136, + 15476, 24093, 81595, 151484, 132986, 88790, 24759, 249112, 10955, 113624, 1490, 61893, 85782, 267543, 81516, 155220, + 34486, 85773, 20198, 5827, 26696, 182958, 47877, 133459, 7991, 47474, 35921, 5055, 28813, 58299, 36457, 2282, + 18858, 199851, 25971, 38032, 39546, 9892, 80469, 94210, 11703, 7530, 262048, 119116, 11365, 73348, 20636, 76662, + 38185, 147830, 33090, 12842, 5716, 136557, 29208, 2738, 93555, 29435, 37866, 29232, 11925, 18290, 200392, 9150, + 40121, 28353, 66557, 42934, 60002, 7949, 165121, 44607, 6985, 46265, 88952, 117927, 10976, 127994, 45713, 196202, + 171384, 16237, 17072, 82678, 3887, 53943, 11497, 25289, 43394, 194558, 17520, 13713, 13337, 10319, 13519, 27977, + 56346, 1512, 69563, 16655, 6381, 207119, 32722, 52407, 252336, 7634, 295451, 137340, 39573, 12588, 51167, 91362, + 75042, 12378, 44599, 44996, 53711, 62621, 59425, 154573, 1028, 6863, 70608, 89277, 31781, 37585, 33115, 45438, + 44295, 19441, 13395, 85115, 95925, 51396, 98398, 31173, 131713, 76310, 2356, 18575, 7214, 48396, 22367, 234325, + 68125, 225602, 25129, 98668, 43571, 24981, 27341, 97198, 70954, 116350, 138790, 6397, 213234, 6496, 199647, 33078, + 1628, 16810, 38131, 5432, 8760, 45379, 187797, 54575, 13391, 66830, 185263, 43959, 65485, 1291, 64470, 193537, + 17320, 32341, 19589, 46922, 56038, 16790, 33933, 93128, 1710, 22162, 60010, 166501, 25248, 110735, 133168, 19601, + 60803, 25674, 33996, 3691, 57399, 83385, 754, 371286, 19839, 26834, 28124, 75623, 3870, 66122, 97406, 47346, + 326, 7492, 166, 56406, 281556, 73496, 63104, 70066, 50945, 17847, 47599, 57275, 72400, 26598, 25290, 133091, + 21460, 6170, 26197, 90337, 316244, 23380, 15810, 44542, 75619, 115807, 61180, 160228, 56246, 187764, 113793, 119939, + 98775, 14502, 129669, 102901, 481, 18772, 33749, 181939, 13513, 80163, 57426, 95677, 55765, 123256, 156375, 131050, + 50427, 85036, 176699, 53355, 7388, 116459, 9415, 65345, 123519, 14467, 53003, 35892, 32929, 29108, 141336, 7280, + 57908, 121975, 16321, 66321, 27124, 17980, 13278, 64623, 61316, 27319, 33770, 106870, 101097, 65509, 32584, 7662, + 57467, 42561, 35131, 72066, 30228, 57341, 330807, 12061, 48792, 30042, 47357, 5873, 10788, 40943, 43598, 71460, + 20402, 3772, 196771, 8512, 37318, 98225, 52470, 20068, 50526, 32163, 6724, 21, 155730, 109241, 153690, 41562, + 34456, 4040, 322, 2322, 158065, 82495, 155777, 84593, 145305, 52472, 4167, 12757, 17832, 20800, 6523, 97949, + 43733, 5158, 13272, 52760, 64052, 64053, 5758, 88849, 4506, 28625, 252038, 68417, 35072, 144810, 45051, 20202, + 205355, 88775, 28776, 58617, 177028, 20250, 77055, 25756, 184004, 13595, 134521, 29706, 187081, 41138, 39350, 69874, + 68616, 45852, 200508, 35308, 35938, 187989, 19158, 97, 83703, 30783, 33740, 8875, 10367, 31971, 9154, 23524, + 36892, 35352, 34135, 16609, 93186, 10325, 61501, 123818, 260137, 38611, 132695, 10376, 51934, 40069, 47671, 13891, + 89970, 27882, 128001, 251194, 4985, 3138, 130144, 157048, 23445, 50690, 120275, 98986, 26755, 15060, 110524, 20070, + 21558, 21044, 241916, 81443, 7948, 110867, 98839, 117178, 78741, 134771, 8767, 9438, 23376, 10179, 78727, 73418, + 124405, 70709, 73897, 174550, 85594, 44683, 44048, 151892, 68596, 110549, 32110, 143310, 57536, 6531, 94425, 3086, + 49351, 16758, 85882, 1621, 12619, 28857, 47203, 30663, 56593, 71986, 13128, 49100, 132668, 101411, 14597, 17845, + 99924, 49653, 254641, 68928, 109728, 11860, 46362, 38227, 71844, 106860, 76428, 22535, 71935, 47939, 100970, 214387, + 196018, 26420, 64000, 108949, 65464, 85292, 57337, 107755, 17617, 127677, 45676, 32224, 14987, 15885, 23365, 73603, + 89701, 45904, 16644, 214947, 143773, 45929, 4638, 50137, 33997, 6717, 227758, 15712, 8687, 13499, 81744, 240749, + 22701, 43383, 31416, 14599, 33258, 22158, 46890, 13056, 2914, 36843, 11727, 56468, 4436, 2709, 77185, 66068, + 72108, 79794, 13174, 6017, 85610, 25092, 117935, 33783, 9671, 20404, 190908, 59000, 49067, 32677, 203049, 32652, + 2327, 64872, 122696, 32308, 82988, 180624, 3995, 109656, 1361, 89288, 58639, 34932, 1672, 23129, 114033, 56989, + 44033, 18540, 122789, 23587, 35777, 124932, 162078, 37243, 30269, 38337, 87579, 108472, 24057, 7318, 56171, 14522, + 10605, 36398, 2675, 68455, 82236, 229893, 28011, 180822, 616, 32685, 24540, 51512, 15793, 167325, 12485, 108625, + 13919, 125674, 46942, 46777, 47249, 192801, 19663, 7112, 112086, 41046, 136949, 15147, 116698, 102811, 103661, 220539, + 9484, 36775, 262931, 43580, 80207, 119020, 20707, 35285, 90574, 74644, 25380, 59691, 33304, 112273, 100827, 37498, + 68624, 28392, 22953, 10192, 93127, 6246, 78709, 81865, 56101, 3539, 141381, 87190, 42210, 15605, 33843, 301219, + 160929, 15074, 16495, 322258, 4827, 113897, 61108, 7004, 38995, 49556, 40164, 186048, 38095, 7638, 85669, 24310, + 4701, 41162, 181937, 6723, 125119, 103514, 13105, 42284, 5027, 699, 66990, 9452, 33680, 16754, 14243, 12215, + 9236, 9693, 21846, 11245, 231236, 26285, 119411, 107569, 14135, 13418, 13188, 12438, 87837, 154259, 13184, 45250, + 23318, 104289, 32255, 140998, 70261, 10596, 72378, 28503, 78893, 204776, 26664, 107431, 30151, 53770, 139833, 72090, + 81482, 31259, 9478, 50013, 14332, 4473, 8040, 96916, 1084, 3459, 15193, 216567, 74256, 68159, 8514, 64488, + 27532, 4063, 51181, 118473, 46644, 135301, 17479, 19499, 79185, 32968, 60750, 136481, 3148, 433, 1609, 42629, + 62615, 19220, 27930, 149236, 6981, 35309, 3835, 52812, 55672, 37988, 64541, 83189, 31668, 21678, 96359, 51037, + 36825, 16611, 55113, 22398, 1849, 16045, 71184, 149329, 14536, 231005, 89158, 27040, 37446, 89933, 98572, 9183, + 14738, 180870, 66669, 119194, 23779, 41333, 133198, 165958, 43058, 12753, 4695, 25960, 162749, 13009, 104735, 168462, + 72199, 56087, 30780, 17289, 1168, 63376, 32319, 148939, 21346, 183499, 57766, 111111, 5592, 163921, 51902, 6385, + 85932, 38348, 98269, 48430, 85302, 6828, 144609, 62182, 9668, 31728, 14877, 74838, 18351, 19716, 44331, 8938, + 57135, 111212, 35796, 196509, 150360, 14184, 81850, 64400, 2080, 43786, 89260, 297313, 228015, 4259, 2273, 106355, + 77953, 79956, 258556, 39080, 34670, 38042, 9041, 17580, 119302, 94867, 12312, 65168, 151751, 14199, 84612, 41781, + 31009, 41498, 2644, 8135, 106725, 11873, 65756, 50677, 84265, 100278, 19103, 25944, 45214, 71109, 29333, 199959, + 59289, 28006, 82735, 73888, 74178, 9357, 31611, 55567, 113071, 91025, 22225, 16280, 263206, 143472, 82038, 1016, + 111009, 3454, 111282, 6624, 4021, 148481, 82606, 3337, 44449, 87085, 117440, 117450, 188119, 96922, 66328, 69088, + 192629, 7164, 177681, 13842, 123592, 13678, 3709, 1404, 392506, 88363, 34011, 3111, 292847, 35632, 135945, 165656, + 114487, 28103, 36070, 105813, 12831, 13767, 283706, 20001, 205858, 222376, 26231, 74767, 9852, 14707, 55251, 21778, + 20780, 81817, 61071, 27574, 79648, 73651, 59592, 26903, 75767, 1269, 47843, 22121, 357014, 1880, 118720, 33888, + 97721, 25074, 162232, 165250, 18166, 101436, 126490, 14600, 21005, 87059, 98216, 138975, 67950, 11357, 183453, 60893, + 33226, 31203, 30167, 34258, 204009, 158681, 64517, 2772, 16536, 8063, 37505, 72518, 2987, 13469, 1060, 70404, + 11335, 58467, 606, 80478, 66717, 28334, 72244, 111835, 144910, 7759, 9382, 16929, 47950, 24768, 18442, 3812, + 17262, 78772, 57860, 139250, 55060, 192875, 157943, 1432, 15063, 5317, 91497, 24602, 2934, 71481, 112610, 7454, + 51038, 67723, 15783, 27576, 26775, 96465, 16801, 41801, 178497, 1952, 4258, 35229, 66008, 12359, 80274, 39191, + 3427, 79381, 23113, 37373, 49433, 369, 197495, 44949, 128592, 41764, 85267, 26089, 20036, 84751, 45080, 183648, + 157553, 6777, 354790, 8756, 63861, 32306, 73479, 133947, 3089, 129130, 28938, 68599, 23015, 83388, 4921, 46304, + 51210, 118961, 162098, 13808, 28696, 56249, 178796, 65420, 35933, 130109, 17053, 154383, 49942, 34368, 45185, 215004, + 71135, 72056, 58502, 92030, 231518, 3061, 31419, 84551, 99620, 59670, 13439, 24874, 17248, 49981, 120, 101486, + 21835, 11954, 21387, 80202, 176536, 69099, 25576, 83929, 20676, 54972, 21235, 121229, 16944, 207747, 17151, 62192, + 38700, 14914, 67294, 97250, 165831, 65091, 88309, 65140, 233253, 78072, 119737, 2388, 295625, 75403, 48047, 5295, + 36723, 97958, 31105, 11399, 13023, 94081, 105941, 87375, 52745, 78093, 142695, 58118, 10907, 36936, 47744, 98453, + 19423, 63195, 135090, 155076, 67972, 116671, 181141, 76674, 125868, 7497, 7282, 18173, 95473, 37926, 40931, 83084, + 82875, 9577, 10786, 82123, 1183, 13043, 25845, 7930, 108455, 108021, 144839, 298080, 52685, 25893, 74303, 83892, + 33417, 167899, 3363, 124169, 64095, 3491, 11574, 30740, 21433, 19985, 31353, 13355, 52438, 74347, 30435, 24346, + 33191, 43579, 92911, 14343, 127809, 55919, 202307, 87794, 44505, 85592, 45, 37946, 211823, 75477, 10315, 11020, + 7810, 8418, 14824, 32246, 2752, 43445, 78058, 51648, 95548, 28128, 139532, 78431, 162031, 54035, 118501, 105304, + 7185, 33563, 841, 39346, 91196, 67423, 12694, 225158, 47563, 63637, 48178, 53247, 61721, 57205, 32651, 77002, + 33359, 256934, 6389, 100897, 17897, 60470, 2058, 38569, 23682, 58623, 3141, 225129, 42806, 36558, 74186, 65297, + 178101, 65934, 727, 11385, 22874, 28253, 252846, 104582, 49707, 66565, 52743, 129552, 199897, 4526, 6020, 48400, + 75419, 48628, 13529, 146713, 82456, 3396, 19131, 77933, 8618, 26055, 136494, 270517, 11171, 161242, 27374, 10081, + 79712, 4483, 132351, 34334, 116847, 98238, 115123, 62251, 18783, 13247, 236033, 17204, 44190, 86807, 31546, 3315, + 46564, 61125, 188041, 5405, 5346, 199954, 2749, 50374, 59046, 179633, 124580, 35456, 95032, 68263, 16037, 104183, + 7893, 68877, 23745, 4536, 14916, 44942, 88134, 154142, 3214, 4074, 126680, 444, 26800, 13417, 122265, 141794, + 24172, 9594, 4737, 198047, 121453, 10533, 56408, 70714, 34362, 74346, 24830, 15909, 10250, 226352, 86319, 65102, + 17408, 113698, 65723, 192701, 18888, 29804, 92420, 119089, 4840, 52158, 6809, 47211, 68696, 127333, 9079, 122944, + 22831, 10761, 65707, 48241, 13162, 83275, 41056, 127568, 36005, 52744, 10097, 2969, 32512, 44770, 40452, 180033, + 14800, 160155, 1451, 16499, 62357, 50678, 47022, 118784, 41723, 26067, 60563, 169681, 45046, 58619, 46085, 32423, + 27247, 64149, 46183, 1743, 37486, 3802, 17194, 24647, 5372, 19756, 118649, 4609, 2564, 83610, 112562, 46541, + 34261, 17420, 81688, 66264, 298500, 194042, 239563, 21230, 66509, 23479, 154376, 33179, 133920, 158984, 47967, 146524, + 89138, 72960, 127604, 25358, 31305, 13512, 95336, 27420, 117697, 9777, 159295, 2091, 19097, 4938, 15518, 81702, + 108304, 448058, 126466, 7371, 81386, 239535, 1607, 211558, 84106, 53806, 374, 125065, 23802, 113479, 52151, 46803, + 46411, 21764, 51063, 22432, 63304, 31295, 6642, 26992, 946, 58917, 74562, 10672, 51417, 1090, 157283, 6175, + 41777, 30873, 49434, 31205, 41041, 70979, 28851, 11292, 19501, 20377, 16444, 125033, 115864, 49848, 35298, 117499, + 60743, 35553, 7657, 117188, 294354, 1111, 19913, 107692, 37955, 25543, 107874, 180298, 94165, 181537, 43123, 3058, + 18116, 7886, 11895, 8962, 1156, 81655, 22350, 1372, 17937, 241608, 114745, 32122, 20645, 31944, 32077, 3682, + 57114, 6050, 45126, 4535, 90804, 42423, 18205, 88298, 58042, 4218, 130816, 58944, 48643, 192539, 3516, 129850, + 92288, 67482, 5645, 6008, 9861, 19038, 25523, 48393, 2557, 163977, 7711, 69978, 88546, 98350, 106098, 5102, + 61333, 26201, 45643, 21353, 101008, 50380, 9018, 90864, 12853, 151908, 45670, 48604, 5148, 32715, 39819, 1925, + 87856, 45697, 2007, 66226, 4152, 9542, 6588, 36053, 144503, 58516, 41489, 32328, 73841, 5629, 33075, 228550, + 18718, 1278, 118540, 138339, 9789, 1171, 99245, 73118, 147565, 168255, 51267, 5701, 10846, 18000, 6368, 25481, + 42085, 30996, 30750, 21874, 12789, 33285, 67958, 162991, 30223, 14020, 26867, 21295, 8993, 66175, 79515, 148442, + 56352, 397258, 104422, 69338, 67203, 81, 103746, 192218, 2448, 103541, 4268, 26498, 28215, 26795, 119536, 166999, + 6052, 116957, 18923, 77982, 23540, 54054, 31372, 18430, 126319, 39890, 1756, 5893, 75933, 40146, 271195, 76657, + 36689, 21496, 89710, 61547, 80235, 15197, 81995, 39003, 23231, 6321, 26632, 19783, 23561, 14948, 63802, 56505, + 21383, 38408, 16252, 72824, 38800, 196129, 74267, 8127, 77548, 48102, 87833, 36171, 102798, 47900, 15495, 173684, + 21234, 30001, 52403, 46069, 31468, 16788, 57124, 45489, 158608, 82654, 59957, 10410, 46188, 171714, 31155, 59538, + 63960, 149773, 98372, 22924, 191679, 82803, 49057, 16752, 8051, 157358, 24903, 23142, 67014, 23023, 30666, 6199, + 11185, 61761, 108153, 49193, 170078, 64795, 286604, 157605, 42186, 22327, 189946, 54197, 28827, 199449, 44686, 59637, + 34777, 152898, 16645, 83206, 41930, 102775, 24648, 36524, 212079, 95709, 1548, 100504, 12421, 68448, 31268, 474576, + 34750, 218355, 92781, 134981, 24111, 67882, 54747, 174240, 110344, 94961, 114973, 72370, 73918, 15923, 15159, 7647, + 45171, 65568, 43530, 34464, 70826, 34474, 60365, 197139, 141949, 210472, 46891, 121996, 40063, 94408, 12981, 134679, + 23979, 51874, 24714, 19995, 24254, 36223, 157770, 7326, 37309, 94133, 22106, 78205, 26724, 2537, 25382, 106656, + 27179, 66311, 110415, 22901, 24718, 57112, 55550, 53766, 83648, 13063, 9413, 49639, 54938, 59353, 51453, 13425, + 14591, 124743, 17157, 12381, 17425, 61233, 6845, 73147, 29525, 129664, 27744, 5277, 102675, 54090, 98234, 13352, + 48975, 17754, 16594, 102750, 48654, 39678, 70262, 34873, 12316, 8508, 46395, 134713, 8929, 37841, 2872, 44089, + 60640, 45964, 41710, 35853, 41709, 32561, 10847, 30533, 50168, 131914, 35502, 7179, 63264, 101004, 13098, 13461, + 89812, 195792, 68289, 50907, 50716, 618, 42226, 56670, 48632, 3817, 23883, 6968, 38755, 21629, 7154, 3872, + 138583, 91142, 62213, 50389, 160123, 68183, 5493, 38, 55579, 58839, 15930, 157836, 71829, 40468, 28030, 16196, + 24230, 33693, 43412, 1608, 233277, 6049, 16708, 17549, 46322, 16760, 153973, 62078, 39650, 117125, 26112, 36797, + 166388, 166224, 101342, 69645, 34718, 6450, 41096, 88450, 24108, 54215, 7626, 56703, 98252, 7498, 5996, 104382, + 7031, 18513, 1464, 20355, 106695, 12121, 15785, 39445, 62498, 113, 143048, 42438, 18258, 69031, 20970, 28430, + 35062, 185809, 306428, 189608, 217827, 3480, 29084, 79447, 78731, 32985, 65014, 127203, 34824, 4550, 257781, 321144, + 33354, 3217, 218324, 88938, 19520, 11596, 43514, 141333, 60852, 65248, 6297, 11095, 2432, 14119, 13748, 7469, + 60224, 12819, 57422, 64540, 8587, 52981, 73978, 23601, 2836, 30776, 142181, 19639, 62955, 10337, 595, 211745, + 702, 140990, 56342, 19255, 20227, 135292, 29499, 348456, 42285, 59021, 23153, 398, 40560, 12943, 20085, 118311, + 95592, 31441, 30421, 18709, 62486, 96516, 37157, 85251, 11094, 115980, 17285, 128523, 53035, 4890, 10141, 87821, + 143291, 78302, 59857, 33639, 18842, 69975, 11769, 10009, 46177, 505118, 54757, 192067, 77994, 12960, 50591, 24130, + 1770, 3662, 133316, 209915, 9657, 48891, 39791, 15630, 107422, 16844, 81827, 8655, 172915, 60826, 36740, 246774, + 32655, 23684, 174097, 93563, 128716, 9168, 16463, 138739, 25886, 3373, 1869, 35842, 25164, 41241, 1126, 17486, + 156740, 84370, 68201, 98020, 119928, 18998, 6457, 120651, 165875, 3291, 58021, 5480, 85817, 5424, 7918, 43877, + 11007, 125299, 24181, 67060, 89110, 233405, 35096, 471, 33956, 19798, 152016, 168120, 12652, 156405, 133111, 116254, + 65156, 42113, 9958, 120153, 180266, 111540, 269252, 123200, 8494, 34196, 8578, 82607, 36889, 12572, 69053, 33057, + 19958, 67796, 22249, 3640, 20955, 15759, 9894, 33572, 96, 8969, 80871, 33260, 1264, 67165, 9108, 87451, + 118288, 65977, 59131, 17708, 135769, 66193, 98679, 61993, 44754, 23134, 122439, 76614, 86671, 113511, 7544, 55848, + 5632, 21119, 183800, 40580, 19026, 82001, 99485, 55491, 168220, 108940, 236173, 16220, 289120, 163513, 75470, 32902, + 33569, 88755, 3113, 96883, 93821, 299927, 114933, 5574, 66144, 80618, 43070, 125428, 70635, 119887, 3673, 55958, + 7687, 101495, 5831, 97794, 5642, 23968, 7105, 10753, 2714, 39381, 104590, 82035, 55445, 30394, 30985, 36078, + 56636, 42720, 49190, 134117, 71545, 2558, 567, 53812, 184182, 144, 41897, 149050, 93133, 9426, 48576, 19715, + 7332, 68965, 74846, 31701, 37389, 27344, 9014, 157686, 12643, 17067, 29166, 15457, 52315, 133218, 97956, 26560, + 22729, 19455, 100687, 66306, 11014, 31241, 157229, 1036, 158742, 1911, 18856, 8404, 17050, 49637, 105257, 26263, + 152889, 33300, 24194, 15449, 50178, 57835, 89431, 93344, 34601, 52068, 6811, 39076, 41945, 41909, 38286, 221235, + 52136, 190373, 62204, 7398, 9948, 20577, 17514, 97470, 26914, 13538, 27727, 115631, 63548, 60637, 26694, 36204, + 95721, 237466, 17635, 8311, 115951, 155407, 372775, 119613, 40759, 7149, 153893, 29766, 8960, 16679, 2573, 75649, + 158258, 17939, 56612, 138333, 38938, 4371, 39651, 46863, 49232, 4029, 76050, 62734, 48325, 4870, 8916, 70189, + 42234, 482, 51761, 85517, 81523, 299332, 91771, 91710, 253019, 32687, 95521, 66713, 66128, 29849, 16707, 26650, + 40978, 43719, 4139, 42193, 20048, 230595, 55007, 13024, 238048, 4557, 73590, 107873, 38760, 29575, 21439, 27927, + 62928, 129083, 22364, 3858, 122560, 50006, 32925, 40998, 118532, 6970, 5412, 10487, 43687, 53543, 72899, 67285, + 137472, 16857, 46509, 32903, 163689, 209351, 336887, 206325, 26680, 147850, 10276, 166671, 9878, 111570, 4130, 56293, + 17448, 129038, 49791, 3323, 51035, 77462, 50111, 20907, 16211, 5982, 24162, 56278, 60834, 28521, 209493, 95578, + 36749, 39356, 52739, 208999, 29178, 58645, 19127, 12057, 14241, 39982, 78123, 80813, 59868, 6719, 15571, 19762, + 150086, 20349, 278029, 2666, 2305, 133772, 121203, 3570, 26477, 55785, 32824, 6151, 42422, 310941, 94923, 18190, + 34342, 18288, 70657, 25029, 165341, 34909, 320097, 33308, 83279, 75404, 45112, 17301, 33894, 4577, 18669, 9219, + 14257, 107253, 512, 36710, 29928, 16653, 7279, 43478, 12743, 3862, 3899, 37090, 13957, 212990, 2895, 53197, + 125571, 40119, 7450, 95395, 89134, 18938, 54751, 39220, 66712, 44978, 98747, 35334, 10952, 9840, 217378, 35750, + 16507, 209521, 52915, 36820, 147839, 56525, 13285, 1981, 30146, 51236, 7364, 104143, 7249, 222095, 6399, 165008, + 16565, 13029, 27969, 105952, 45399, 41770, 169129, 53714, 39874, 7853, 68938, 61382, 114565, 59737, 77275, 29135, + 215780, 6342, 153711, 36717, 31990, 50742, 2834, 52961, 230881, 31804, 87897, 73954, 171477, 9339, 91416, 34110, + 102, 54930, 1613, 11876, 196546, 591, 20460, 50406, 44538, 17056, 34267, 74596, 10880, 9193, 96244, 29689, + 84948, 20424, 26253, 45632, 281705, 119042, 185961, 44497, 86651, 192207, 11795, 114742, 10617, 265712, 62568, 92499, + 31395, 217897, 187475, 50383, 2342, 23312, 42405, 26096, 453658, 416, 133446, 93547, 43569, 10623, 121978, 21757, + 60561, 74826, 42853, 104403, 132901, 72040, 67234, 59013, 21845, 102518, 153642, 11146, 17727, 102632, 86313, 72775, + 58556, 407048, 21027, 6844, 258242, 89512, 3527, 107940, 22262, 23956, 4850, 30270, 58728, 16998, 117875, 20689, + 4008, 14646, 93000, 1170, 77997, 167859, 54531, 246835, 11806, 26637, 167713, 64014, 37431, 88103, 62362, 304585, + 30599, 44120, 31653, 42563, 81375, 43712, 1534, 49941, 109137, 145866, 103885, 146125, 185787, 22202, 33673, 3681, + 114085, 22277, 2072, 10477, 217292, 47874, 25630, 61064, 97453, 94351, 180344, 34224, 169085, 68184, 1881, 53451, + 30593, 100128, 39311, 3509, 60212, 90102, 7297, 23473, 11544, 261844, 8370, 27945, 102056, 100221, 39918, 115112, + 65580, 27264, 10582, 66982, 2384, 29292, 51255, 8837, 91655, 60384, 127046, 34788, 4855, 82574, 50338, 58959, + 95725, 135206, 28047, 308666, 7295, 41193, 95306, 61581, 157994, 185542, 36976, 28033, 16228, 38830, 10448, 15213, + 20669, 49883, 2493, 9951, 53276, 25223, 13971, 17396, 141590, 48264, 32377, 28668, 105246, 22993, 20557, 139621, + 17334, 37996, 19658, 20744, 25440, 34122, 116482, 22910, 76067, 36796, 67819, 11102, 17967, 9536, 55606, 117501, + 39321, 17563, 65478, 3571, 38911, 73504, 54583, 10444, 11362, 66927, 89439, 14702, 28559, 7682, 1513, 70102, + 63807, 20057, 46270, 21001, 21627, 256959, 184942, 165920, 26468, 36183, 112227, 4448, 85816, 65617, 77464, 8502, + 40120, 56527, 77638, 3102, 1025, 32749, 21811, 153766, 15234, 14725, 328941, 53852, 58319, 90203, 39231, 68729, + 69516, 3450, 70578, 52687, 66512, 8474, 90859, 4090, 124548, 66064, 39981, 28642, 75845, 40634, 22064, 107475, + 78873, 112238, 18036, 60258, 22137, 89278, 35142, 92257, 46681, 60200, 168934, 5658, 51242, 3324, 71459, 64772, + 85683, 32, 475, 18068, 32909, 134334, 1380, 31213, 76747, 9749, 4201, 23438, 35555, 83518, 23878, 30619, + 43396, 224662, 33147, 28171, 101465, 60063, 34256, 71664, 1765, 160809, 93427, 22239, 73094, 35152, 17172, 38091, + 1077, 83764, 93545, 57605, 2962, 9185, 13119, 81080, 39028, 85967, 49835, 9330, 66777, 27605, 184827, 77443, + 57831, 49230, 57654, 174766, 42048, 200323, 166050, 27249, 15828, 76305, 4194, 44524, 13962, 51363, 67697, 14701, + 36041, 69963, 72263, 4470, 63657, 47301, 159567, 22495, 52412, 69094, 411, 3653, 5242, 4607, 116518, 99099, + 58846, 58552, 89470, 53229, 2141, 273007, 27018, 137017, 5506, 4365, 159178, 120422, 219012, 127162, 69641, 26407, + 134451, 42927, 9321, 12622, 3936, 145843, 24141, 64837, 182230, 170529, 104842, 135255, 17558, 17348, 53327, 12872, + 17153, 10391, 30, 74060, 152237, 14185, 205380, 31566, 22621, 30796, 108736, 26638, 49377, 673, 90154, 51194, + 170216, 63571, 45050, 32131, 35257, 17360, 55199, 20393, 68233, 118377, 63209, 4669, 65374, 65781, 84411, 92579, + 6510, 108160, 56952, 192831, 11126, 55557, 110245, 73583, 212151, 16429, 369879, 20590, 7184, 24497, 41359, 86934, + 2480, 131718, 116501, 37833, 22517, 9731, 10757, 78661, 3437, 19565, 6471, 13896, 33073, 74906, 286, 53375, + 30156, 29608, 44034, 13989, 16557, 36336, 11786, 9016, 3768, 30883, 52210, 33318, 55067, 37827, 143891, 319150, + 86829, 93201, 25144, 48395, 91000, 56891, 94800, 60129, 12350, 105311, 185, 78672, 148650, 2793, 13582, 54814, + 66017, 22898, 190292, 69218, 79424, 1785, 10144, 90001, 70885, 6226, 65791, 123504, 49066, 19757, 151381, 633, + 28250, 21135, 4879, 91112, 21369, 79333, 54846, 138302, 51213, 7684, 96590, 49597, 34533, 30172, 13591, 193897, + 11510, 1702, 73010, 42908, 3258, 13889, 151689, 47726, 18176, 159526, 179101, 31058, 18465, 26752, 188829, 15744, + 84413, 58916, 38458, 15452, 6315, 50155, 89783, 5933, 36411, 8754, 11471, 15409, 163765, 12966, 2808, 36697, + 4346, 161209, 70415, 56116, 356, 211098, 14356, 36846, 107618, 53672, 48457, 30017, 598, 176513, 81011, 18982, + 13727, 86974, 48130, 58055, 285026, 119331, 69388, 22734, 162695, 127129, 6676, 10209, 8749, 19120, 28354, 35996, + 14583, 2294, 21774, 7098, 7132, 30506, 68020, 26105, 63521, 287047, 31652, 29144, 184253, 45019, 59739, 54276, + 32378, 4681, 28618, 155387, 25991, 60268, 28085, 115514, 5604, 89273, 73475, 54698, 30961, 26093, 14648, 29343, + 53675, 45157, 6691, 46637, 4874, 13617, 45597, 95292, 84693, 71181, 195063, 4205, 5086, 45424, 49483, 57401, + 34811, 81433, 10920, 60312, 26978, 122016, 164823, 61302, 56564, 35695, 36714, 67995, 7904, 42503, 11148, 636, + 33519, 45175, 78078, 5252, 51221, 95264, 51178, 64304, 113942, 105499, 51841, 54146, 69253, 1646, 75338, 105535, + 6664, 10892, 158642, 62272, 125563, 215912, 33549, 19391, 22055, 105382, 20979, 20764, 220680, 38079, 4062, 13641, + 102008, 4203, 20143, 28206, 742, 47639, 13458, 8201, 51930, 16197, 22187, 25787, 19494, 75134, 114304, 94871, + 176108, 45555, 116, 160484, 44424, 12397, 146985, 10330, 35123, 239546, 28027, 7835, 13025, 82191, 412132, 7092, + 75685, 170210, 25010, 106234, 11759, 43065, 8341, 715, 74335, 50832, 12326, 62728, 22250, 132367, 15166, 1485, + 181453, 100489, 159053, 49558, 131147, 218225, 72843, 179732, 16984, 57645, 5036, 111986, 132115, 50930, 114625, 1202, + 154311, 67481, 16722, 47767, 11991, 40423, 208598, 57602, 76452, 34425, 3714, 48128, 52609, 81295, 9952, 36845, + 85351, 16065, 20654, 19823, 196, 24873, 26870, 1297, 30969, 9296, 54228, 102469, 9198, 72519, 70039, 108396, + 74919, 85172, 66125, 101377, 2529, 117805, 96810, 24778, 56838, 145070, 22575, 3889, 71779, 55500, 69200, 56256, + 29187, 1545, 88869, 2923, 116304, 4719, 101420, 2691, 3504, 4334, 13196, 24283, 62330, 11167, 46433, 98976, + 41190, 4304, 38379, 134621, 86153, 55398, 18685, 16560, 28393, 104952, 236521, 190161, 254926, 97238, 29381, 35076, + 104228, 132048, 21165, 43831, 105189, 81338, 60610, 75143, 13264, 30942, 42614, 10992, 84359, 171469, 36755, 2955, + 3574, 50858, 7720, 113363, 12415, 1082, 8309, 16640, 8534, 89729, 1994, 39280, 57147, 259158, 159887, 14424, + 10175, 59116, 66944, 434056, 188174, 112776, 24021, 39757, 59504, 17275, 18210, 6474, 60932, 53833, 48968, 4909, + 66318, 8851, 66549, 14450, 16407, 3384, 60036, 132244, 107921, 15561, 173708, 120543, 17638, 31452, 27253, 351, + 99103, 3968, 104995, 54896, 49278, 134414, 51028, 59123, 28403, 11528, 46089, 883, 39786, 58165, 83022, 52349, + 145865, 50737, 25618, 47214, 8462, 6871, 83132, 9635, 3558, 132475, 8680, 97166, 43406, 3898, 139071, 89536, + 142271, 8320, 29089, 83232, 29139, 37374, 11887, 13573, 21989, 22330, 18987, 8277, 36552, 94008, 13402, 19357, + 93955, 192242, 23973, 19276, 72365, 83440, 43343, 20913, 7176, 15598, 107084, 9241, 13556, 46936, 46456, 48864, + 106185, 54904, 62326, 15469, 37957, 3365, 221144, 71002, 321774, 79203, 35192, 114690, 17782, 21700, 10335, 97643, + 129017, 125386, 115842, 123788, 51154, 15859, 74495, 30765, 27938, 35855, 55933, 25082, 24952, 27639, 71742, 23512, + 1935, 32522, 98612, 72854, 39366, 29090, 7051, 75719, 2791, 156786, 23966, 41744, 33489, 127417, 36727, 13017, + 41582, 128016, 125842, 141281, 56078, 108220, 31920, 77429, 24558, 59290, 9558, 18760, 9311, 51229, 19776, 17252, + 5449, 45702, 18396, 21552, 218786, 72761, 87355, 23965, 27808, 90969, 38872, 26050, 190429, 178071, 13825, 15009, + 68013, 27591, 38972, 14110, 36020, 50172, 200538, 281255, 86003, 53465, 47708, 215042, 29735, 28534, 1368, 69656, + 3404, 24318, 47650, 37653, 87348, 40656, 8697, 25748, 119357, 39951, 51347, 22849, 115714, 1560, 13392, 73201, + 2324, 35889, 27677, 41987, 86796, 378138, 63587, 34445, 81973, 5437, 94808, 11707, 40992, 27780, 19572, 62130, + 43376, 35564, 204011, 21987, 93621, 4561, 146551, 233073, 28784, 56146, 1533, 99739, 16808, 78879, 204037, 27916, + 36367, 31905, 55776, 19527, 2517, 49088, 49061, 67853, 2909, 67751, 72529, 12945, 191926, 57369, 47731, 345715, + 24978, 37675, 6961, 26371, 55303, 17783, 45945, 57303, 53308, 102816, 50376, 152609, 205724, 29088, 100615, 14981, + 60068, 20755, 67912, 22984, 3098, 119389, 76407, 31834, 21375, 86387, 71132, 54368, 64784, 154591, 7007, 38915, + 23949, 15688, 86044, 32410, 26579, 72957, 140702, 2534, 63121, 109577, 36571, 7087, 12319, 38027, 139635, 69422, + 80145, 58104, 77565, 134884, 39967, 128121, 122239, 98149, 97861, 77841, 122108, 53941, 6757, 105412, 5433, 45309, + 70143, 41083, 108846, 68195, 67642, 48548, 22786, 15898, 37082, 16924, 108012, 18926, 34698, 130920, 7208, 84736, + 69140, 115981, 3166, 30758, 122883, 16988, 18278, 9920, 46151, 31634, 162250, 22353, 62187, 128316, 15612, 158856, + 80934, 678, 6963, 76542, 429, 111036, 69613, 18534, 19437, 41943, 186274, 23104, 135071, 64054, 152408, 69055, + 137885, 55427, 70719, 41478, 222647, 50820, 94953, 48990, 13331, 245898, 18180, 15149, 154065, 124925, 217145, 82559, + 327, 139288, 191206, 14470, 61778, 6536, 73668, 129358, 74257, 25611, 26141, 105172, 40116, 65034, 30807, 96573, + 37493, 33559, 28049, 4551, 14855, 7513, 48252, 20443, 85079, 69697, 8617, 95869, 237641, 48366, 36858, 128756, + 42342, 3347, 3587, 16403, 102164, 201481, 17664, 68064, 199965, 62177, 224396, 15650, 71204, 12089, 125579, 76206, + 4662, 192158, 115192, 18064, 29368, 28388, 88784, 93189, 5042, 144274, 26733, 112088, 113914, 8714, 3328, 85297, + 122214, 129118, 14482, 6034, 8955, 1500, 17059, 143211, 13149, 46390, 180700, 11460, 102503, 31711, 124947, 75478, + 43173, 42126, 146701, 30953, 5659, 29827, 78342, 38880, 163787, 38389, 57177, 12232, 69003, 35027, 31713, 32846, + 307084, 9190, 1177, 53022, 63392, 36783, 49720, 204857, 171080, 2218, 24668, 46908, 21390, 44902, 5316, 16460, + 81918, 10429, 22493, 40140, 86666, 28478, 19197, 39367, 36622, 197757, 35536, 74, 24126, 27886, 49478, 60977, + 28887, 159663, 25478, 34942, 5736, 100422, 11653, 280716, 28054, 76213, 269086, 56854, 207170, 12900, 26987, 34564, + 163428, 35718, 204153, 14178, 79891, 154374, 93383, 82977, 346467, 2260, 194527, 234413, 95363, 223815, 84524, 24639, + 38980, 45400, 11669, 39361, 111806, 107478, 23180, 284178, 80828, 13760, 60535, 160002, 9200, 111334, 9377, 23027, + 19288, 10467, 219338, 9403, 294896, 38891, 13604, 6964, 114468, 33089, 246026, 89623, 87405, 73198, 53282, 51034, + 111715, 3542, 21564, 14671, 141705, 108308, 240720, 1039, 7015, 84375, 20894, 62644, 72754, 61580, 52142, 32302, + 68463, 27046, 8380, 114459, 48738, 174003, 101876, 70865, 243147, 15573, 1008, 73339, 33397, 75662, 295288, 97483, + 101210, 83260, 287998, 315229, 37051, 33599, 14151, 14676, 98801, 6611, 61888, 45855, 82847, 52987, 104357, 56918, + 20397, 29312, 1971, 1398, 4940, 37732, 8028, 358536, 185559, 183958, 188192, 3892, 18716, 73119, 6906, 4369, + 54718, 83934, 109096, 203506, 83491, 133450, 4849, 4027, 11725, 13003, 142499, 87752, 40803, 56197, 37560, 36968, + 1128, 46164, 119031, 23432, 12128, 100279, 84518, 28176, 23060, 41295, 20799, 1558, 5174, 23677, 97773, 39548, + 7745, 10446, 105114, 10769, 67007, 87718, 236680, 161719, 46701, 32017, 4931, 19125, 1571, 28423, 43674, 64085, + 27807, 77944, 8529, 13223, 180186, 17974, 13548, 140128, 256996, 51253, 43890, 57544, 18975, 7744, 44658, 28313, + 16837, 105, 127130, 126370, 7877, 20389, 133921, 174075, 212758, 64619, 11797, 47857, 250379, 30209, 1917, 86594, + 15440, 21744, 135164, 34325, 87954, 11946, 12540, 311, 57755, 56815, 231078, 40529, 24719, 141657, 31438, 81877, + 124057, 251942, 13765, 33986, 83461, 63216, 7231, 198398, 258, 90044, 88181, 173923, 50864, 61645, 10174, 54553, + 8874, 28481, 1537, 50513, 29038, 39872, 84988, 57246, 71289, 12015, 94709, 4871, 31627, 279590, 81311, 43545, + 15429, 59109, 71443, 10416, 9005, 6896, 26184, 104749, 4061, 38580, 23344, 2875, 113851, 13804, 63848, 29618, + 107716, 136811, 54885, 33689, 82819, 50624, 149592, 114467, 13651, 11306, 73580, 85777, 79656, 166366, 222515, 39565, + 117851, 47704, 43581, 137265, 17539, 25867, 43261, 1034, 111446, 117360, 32008, 3664, 12938, 250542, 16992, 14203, + 39724, 83746, 187150, 20582, 190787, 110150, 10914, 42740, 4352, 122213, 31831, 49606, 15402, 8583, 34572, 18519, + 21070, 72128, 605, 14069, 62708, 68579, 26360, 45158, 8539, 33700, 23921, 174967, 23777, 21086, 47035, 21403, + 73853, 29070, 14402, 55968, 13552, 25123, 132516, 99114, 38810, 83727, 93287, 108584, 86117, 4037, 69026, 148748, + 16285, 151286, 2526, 67336, 56400, 114949, 38156, 64061, 1718, 14168, 68963, 19987, 75342, 72606, 92460, 25819, + 142863, 865, 10759, 102417, 29033, 128297, 176851, 239106, 378, 3476, 126764, 880, 110113, 35294, 142380, 60787, + 180321, 12139, 33148, 3243, 32586, 75807, 8895, 69743, 23606, 76161, 0, 32234, 26393, 15253, 101159, 37444, + 160984, 100458, 106798, 118153, 207987, 94453, 31068, 199369, 23783, 55406, 72063, 196753, 8406, 65985, 6649, 169369, + 16904, 2912, 14378, 60219, 24596, 44552, 22129, 79241, 47274, 46556, 155072, 25, 11693, 93339, 175196, 82189, + 46539, 25983, 55777, 39522, 60524, 133678, 18470, 77553, 78595, 83411, 33676, 121561, 48423, 93395, 74963, 17181, + 31718, 50927, 45519, 1283, 20170, 4973, 55435, 106949, 9009, 50903, 7663, 1685, 146268, 116269, 1759, 101910, + 15183, 77271, 25151, 101108, 191060, 44211, 30237, 101323, 172765, 81369, 7540, 8670, 1349, 2983, 72423, 57110, + 138436, 10840, 116808, 148570, 37365, 13235, 144011, 326438, 10970, 137859, 245169, 166948, 40509, 8825, 6884, 140489, + 225817, 339638, 308668, 14440, 20021, 172262, 20842, 35348, 70394, 77862, 44480, 114795, 152138, 205967, 80249, 66813, + 21541, 21337, 74233, 246096, 66559, 255232, 7873, 74405, 66544, 78592, 41670, 16122, 89352, 272783, 66583, 16290, + 2725, 97449, 74073, 152635, 17258, 14662, 164479, 7400, 91345, 77718, 116341, 23495, 7313, 145703, 66781, 95536, + 3815, 59962, 62075, 48667, 115868, 88403, 11136, 31573, 8660, 48458, 10303, 30913, 40362, 76502, 54728, 210919, + 4071, 10395, 14132, 19281, 103524, 46479, 2475, 9346, 39388, 11097, 7103, 61594, 118275, 49094, 17865, 16935, + 21950, 49474, 38996, 44270, 6549, 83808, 6663, 54784, 38226, 40402, 13514, 51690, 32754, 33017, 146868, 3834, + 209574, 31460, 2290, 5991, 29201, 189025, 95100, 116204, 43495, 37630, 621, 42790, 18028, 147636, 74713, 72101, + 20296, 39620, 52377, 149079, 40597, 71087, 48840, 41861, 18370, 78232, 9624, 43707, 47520, 67447, 30945, 84196, + 202450, 110891, 61689, 177365, 24134, 22168, 57538, 47780, 2219, 96079, 30892, 4049, 8195, 17695, 2313, 196966, + 69545, 7785, 159038, 116984, 38041, 85191, 95576, 35677, 136934, 87868, 8522, 47264, 46374, 90011, 2454, 121060, + 19041, 15646, 218596, 20647, 159811, 5273, 78222, 5790, 84865, 690, 36264, 169096, 58620, 49769, 19870, 56574, + 846, 44944, 30096, 21418, 98749, 62549, 49755, 90559, 13569, 65527, 23045, 35183, 30714, 1120, 129237, 133919, + 97246, 73893, 1242, 5886, 32186, 73097, 9292, 172076, 4479, 48341, 159618, 10702, 27355, 9327, 3896, 145089, + 92973, 127562, 80631, 140609, 35214, 40087, 6621, 60953, 151491, 93233, 133991, 5681, 75963, 13834, 189749, 48238, + 37631, 42943, 70301, 22025, 1561, 101790, 52288, 23519, 27200, 15594, 330, 128333, 238083, 77116, 27436, 14656, + 23182, 176130, 43018, 93766, 60756, 68727, 36117, 110670, 12291, 90265, 17003, 86775, 25766, 50378, 97783, 72630, + 39355, 51597, 76915, 9532, 102333, 333464, 26234, 91954, 87362, 18461, 14063, 123415, 65741, 87119, 13821, 69651, + 59906, 29250, 5334, 111676, 19538, 14466, 71314, 49631, 201575, 100994, 81169, 99402, 48772, 1958, 86381, 84313, + 102938, 3927, 14847, 115137, 24438, 1719, 106468, 14344, 292580, 113808, 25709, 4422, 39964, 14448, 9781, 167464, + 66366, 38612, 21306, 135701, 9004, 3967, 14882, 60987, 61379, 70755, 79621, 230123, 50548, 251137, 45013, 68059, + 37622, 8601, 52714, 131016, 38732, 161564, 5054, 17043, 28379, 100315, 11542, 22026, 68180, 40581, 195387, 5840, + 76622, 286510, 15220, 21299, 17488, 10435, 34592, 77414, 69849, 292262, 92100, 61536, 5963, 40657, 72834, 85029, + 7219, 137580, 32702, 24398, 48143, 18315, 7679, 10221, 43413, 55964, 56264, 22174, 55358, 421, 32944, 33586, + 540, 28638, 297324, 19055, 58691, 13123, 1457, 13545, 29506, 12292, 90917, 28767, 19245, 95156, 15265, 9677, + 52193, 329199, 120474, 31550, 48621, 32454, 173844, 6940, 5518, 104662, 27465, 11002, 13048, 23406, 127455, 219189, + 118625, 966, 55994, 71978, 44755, 44425, 58484, 81947, 191081, 71933, 4128, 149448, 42061, 45036, 34993, 81007, + 89197, 17973, 12644, 20286, 2259, 12086, 128406, 38420, 60665, 7439, 63696, 63513, 66994, 154461, 3204, 269842, + 71210, 72373, 47920, 147744, 51232, 11668, 11964, 5455, 3585, 62466, 87325, 135079, 142096, 6038, 3351, 59851, + 55024, 90797, 96831, 151965, 7892, 59192, 18848, 84506, 8345, 62542, 50046, 26538, 58653, 123823, 70584, 33087, + 463307, 52217, 94498, 162262, 65658, 37813, 1798, 133237, 64319, 20468, 342279, 2483, 137941, 174532, 134882, 13053, + 136323, 101319, 14858, 315661, 53499, 38349, 1883, 36549, 12746, 4783, 17315, 26629, 43492, 1433, 30848, 103477, + 6978, 53491, 254027, 59138, 95163, 23047, 130383, 71358, 29925, 104048, 147110, 20605, 60175, 141493, 1502, 26372, + 5128, 8043, 160154, 205752, 7352, 24589, 41005, 8787, 41463, 180084, 35056, 128767, 184756, 116446, 216131, 52109, + 121146, 31824, 120507, 78611, 20473, 80909, 52814, 53045, 18426, 47765, 19434, 251918, 4598, 37752, 2736, 30186, + 5309, 104505, 61873, 39702, 54580, 60995, 6795, 78461, 14277, 102209, 56595, 76365, 121151, 84271, 40571, 22053, + 10691, 60167, 8833, 12920, 56711, 2593, 16106, 101890, 43880, 47882, 105443, 26296, 63409, 2286, 236404, 3535, + 76682, 31275, 91877, 94825, 11830, 60322, 77261, 14017, 172218, 120226, 23764, 296, 264898, 128743, 243138, 11474, + 32632, 32455, 30389, 37844, 66536, 221177, 32878, 17990, 81062, 25157, 197499, 6108, 31649, 68174, 4883, 39604, + 25788, 24670, 22233, 11063, 92774, 167596, 40860, 103202, 60222, 67201, 150525, 69109, 11100, 21724, 207627, 23699, + 63159, 50235, 17317, 132722, 9432, 137156, 14594, 85114, 224657, 60669, 32727, 134466, 25240, 42617, 6453, 86119, + 53613, 52551, 39633, 95001, 152, 22181, 33930, 6888, 138620, 139630, 14275, 67967, 163829, 44096, 23693, 35554, + 2397, 2230, 20289, 26508, 85345, 44002, 373, 23558, 12501, 6393, 128738, 2065, 37507, 108535, 43648, 37304, + 64932, 28919, 39223, 9316, 38575, 60236, 72946, 52874, 43522, 3500, 42635, 18532, 65789, 350700, 113725, 232391, + 80198, 62151, 18623, 5216, 78796, 80102, 187580, 46871, 35226, 102412, 1673, 53825, 3851, 294484, 119721, 213745, + 108891, 1551, 16270, 77, 73311, 86689, 90501, 11580, 3060, 174403, 54046, 2060, 28391, 42872, 40600, 5734, + 93671, 48215, 33532, 19766, 39663, 848, 33334, 33841, 46142, 2841, 116488, 9276, 30982, 41267, 161026, 52345, + 66041, 22012, 75546, 14211, 37281, 137475, 48692, 71432, 68157, 27935, 25905, 26558, 26553, 91667, 162874, 41931, + 71872, 49342, 134603, 3964, 81142, 58684, 69664, 61624, 211527, 194930, 43281, 38136, 39747, 141202, 109912, 103720, + 118119, 105830, 244717, 53752, 22695, 3660, 15950, 115237, 2859, 29995, 32157, 26681, 11066, 63677, 2677, 27475, + 20232, 20055, 83960, 187268, 168911, 71409, 21339, 67656, 7933, 3860, 3943, 4936, 197005, 39134, 94952, 21684, + 17066, 56970, 61053, 6076, 111071, 9161, 30747, 75947, 44434, 9778, 40744, 3960, 133994, 16681, 158292, 120209, + 120798, 124190, 20560, 28017, 12766, 9520, 51106, 98867, 227798, 634, 8951, 16361, 45756, 34211, 71984, 20818, + 132852, 2771, 8039, 31695, 29917, 46819, 43140, 4911, 36076, 31350, 146547, 12832, 55352, 87682, 102259, 20181, + 65281, 125529, 76789, 74087, 129800, 20270, 210263, 10610, 41958, 199960, 136842, 106466, 18944, 91106, 109596, 45385, + 84678, 1826, 26239, 13942, 18580, 74780, 96474, 109106, 168093, 58817, 77576, 139329, 132621, 264328, 131918, 3004, + 39997, 277046, 10657, 188062, 54092, 9389, 208482, 138342, 27740, 112130, 141428, 159919, 32354, 78912, 10848, 248488, + 3770, 58675, 143326, 309222, 114118, 121341, 21213, 104326, 103242, 247904, 10251, 17363, 43918, 50107, 99756, 9914, + 15899, 1529, 57591, 10958, 18574, 191645, 27047, 33819, 145944, 50973, 10931, 515, 3190, 56895, 26750, 118357, + 123469, 6582, 232402, 185264, 219903, 14816, 104060, 2257, 24169, 2672, 34078, 85037, 100571, 54688, 18794, 11296, + 62403, 48268, 85546, 58594, 16776, 5744, 7336, 60494, 92779, 55136, 31725, 41292, 14535, 43085, 170495, 62016, + 17168, 20160, 13301, 31775, 16475, 23866, 68028, 7708, 14304, 27024, 412982, 50360, 37231, 31172, 8313, 18133, + 1712, 149874, 165108, 53176, 28218, 60812, 11433, 110651, 242754, 239758, 103026, 54413, 61688, 60159, 42554, 63279, + 28980, 26817, 24147, 239906, 1318, 94868, 66949, 9143, 51359, 25429, 40100, 150526, 222657, 6742, 15209, 35877, + 99200, 23325, 5454, 116860, 67989, 20127, 66101, 152710, 31772, 35795, 93237, 22499, 23932, 217156, 63347, 85225, + 35351, 44967, 43097, 63722, 201251, 232588, 42922, 76661, 49041, 51431, 91264, 26250, 27306, 16146, 171707, 8346, + 19128, 15883, 46818, 14147, 40135, 98017, 121209, 5750, 3986, 4803, 194061, 109091, 77333, 135407, 15726, 139517, + 19649, 240604, 296254, 18468, 120683, 261045, 15054, 49607, 151927, 90876, 33988, 94815, 21081, 4338, 3114, 25278, + 7076, 168237, 197598, 5263, 78375, 28858, 96610, 1656, 77501, 139877, 164638, 29453, 101599, 77549, 15067, 106520, + 8011, 191624, 67048, 47927, 89585, 15567, 16358, 40740, 96715, 19262, 3368, 257227, 58179, 82342, 164701, 77237, + 5378, 183488, 891, 51832, 102138, 1421, 126828, 72223, 106793, 152999, 9464, 38181, 26051, 369537, 47723, 127195, + 217276, 19701, 87052, 2899, 4197, 91519, 1222, 71232, 16297, 58674, 236388, 7640, 27014, 38262, 53246, 56437, + 46721, 45960, 154554, 93517, 13322, 57858, 23476, 62092, 22806, 29494, 60980, 59522, 5278, 152497, 10089, 42687, + 29629, 152505, 1415, 65715, 70632, 27834, 5144, 69061, 9647, 106305, 17509, 83636, 71519, 6066, 32864, 49020, + 58818, 62070, 83328, 51524, 40603, 27209, 14130, 13006, 128530, 34289, 191758, 175447, 8903, 6644, 258111, 1630, + 36770, 19694, 13099, 93412, 56900, 8695, 92739, 9361, 31483, 22727, 24402, 12783, 26935, 64112, 50162, 73354, + 43845, 97824, 38035, 43333, 34265, 4521, 118246, 36777, 34920, 27802, 62580, 75613, 87658, 36480, 37311, 41140, + 6114, 54859, 37921, 25174, 84767, 123137, 100067, 76401, 64250, 31608, 5782, 50390, 47318, 8602, 146787, 57571, + 50720, 184949, 26374, 49411, 19264, 59669, 72340, 61, 162514, 37868, 20961, 23322, 33357, 104140, 12370, 75813, + 13117, 173944, 10984, 8143, 6705, 185922, 4223, 25479, 46696, 78515, 10296, 173037, 75032, 3611, 146130, 87547, + 71054, 20284, 76208, 179291, 87004, 88099, 25915, 10612, 42035, 58634, 8935, 45162, 69138, 29960, 4954, 9827, + 11903, 75332, 189060, 145940, 99854, 68852, 2428, 35935, 102328, 55387, 95597, 31446, 19611, 66526, 25614, 75226, + 34525, 91178, 34821, 57507, 69312, 5965, 54613, 73567, 6431, 38968, 305485, 72661, 49842, 46229, 13066, 35961, + 101600, 6117, 6625, 94731, 133178, 5938, 14272, 71096, 108751, 55431, 8310, 74568, 41829, 15997, 69572, 115173, + 89939, 33817, 27169, 164256, 225664, 113979, 208705, 24886, 48916, 4745, 105265, 2285, 99556, 76020, 16691, 17685, + 9195, 67243, 29412, 15704, 130387, 12347, 87154, 6073, 5960, 22667, 45766, 126328, 36857, 50552, 563, 58566, + 116724, 31042, 63335, 55856, 53518, 24055, 44320, 132215, 94002, 7583, 31931, 161536, 39077, 14213, 37853, 130359, + 53996, 47829, 71470, 59662, 6945, 70919, 15101, 29373, 22261, 65721, 32624, 111143, 64291, 96324, 23614, 39737, + 8314, 9557, 132103, 65689, 152785, 54960, 122712, 73272, 57588, 3782, 8427, 101263, 16522, 3063, 1332, 9763, + 9091, 103598, 131811, 79941, 5048, 53350, 22985, 72314, 87671, 137942, 20832, 32726, 35441, 97544, 14838, 32661, + 39509, 25235, 12815, 229677, 1945, 18478, 104804, 53001, 12423, 99415, 90412, 47427, 158923, 19643, 66679, 33927, + 178413, 13851, 2378, 163553, 37549, 4735, 113147, 13151, 14095, 34868, 84260, 49719, 1475, 55719, 70677, 22297, + 73188, 7304, 101811, 35472, 62878, 110472, 31193, 7011, 4819, 44319, 37242, 22235, 24012, 34311, 34630, 8117, + 68534, 47855, 55542, 88606, 42606, 10807, 64311, 44304, 4010, 54889, 48956, 37274, 120809, 12117, 36576, 15154, + 57497, 55581, 76707, 88824, 59564, 29146, 75878, 346295, 101758, 137754, 84121, 153651, 103718, 85489, 61427, 59883, + 32701, 4692, 17006, 92343, 80116, 63122, 3829, 201429, 12345, 10158, 29504, 164968, 95834, 119755, 125824, 142638, + 46918, 23943, 90123, 20494, 21468, 29167, 157195, 24079, 53213, 49163, 19311, 138007, 15665, 149198, 44724, 55347, + 31200, 1573, 21255, 26337, 3867, 100570, 205427, 109262, 5140, 8979, 83224, 17644, 96013, 1279, 32509, 16380, + 250744, 103649, 111338, 4321, 21016, 68917, 10756, 39197, 10069, 10563, 184865, 35905, 13968, 1109, 7847, 19871, + 35449, 21656, 7996, 38626, 180829, 25293, 37599, 10356, 27683, 46005, 32258, 8111, 39704, 15702, 161889, 13627, + 59956, 21006, 29672, 64295, 22893, 319443, 755, 33239, 3115, 11630, 35242, 316161, 26293, 180051, 34293, 25262, + 32785, 45248, 4291, 1345, 75934, 380808, 185068, 5400, 62445, 95085, 113696, 37657, 141162, 2763, 1716, 31145, + 62720, 101394, 197152, 222158, 2018, 103950, 53054, 8291, 83638, 37618, 74005, 127265, 19949, 171632, 21168, 31182, + 114012, 109942, 16057, 103239, 95006, 48470, 141582, 50740, 3330, 57743, 91063, 68640, 99829, 25131, 192726, 1408, + 130935, 113922, 160076, 66999, 309272, 153746, 62089, 54683, 9565, 23036, 233538, 39614, 55874, 51238, 28998, 51475, + 121727, 56411, 33932, 53786, 37017, 49406, 91778, 26837, 23586, 252174, 2540, 47569, 319858, 177485, 2308, 5581, + 40970, 118880, 34878, 1602, 27602, 64100, 36001, 13488, 8625, 28038, 116561, 45356, 112329, 100, 159062, 48033, + 61060, 53312, 14278, 30173, 100088, 27580, 20456, 230013, 118525, 51822, 34883, 100756, 25922, 52426, 18317, 13881, + 16232, 8187, 317935, 69863, 1907, 53514, 75569, 36902, 60671, 5105, 60024, 76920, 51583, 106419, 20458, 110614, + 44553, 36111, 187025, 173919, 80993, 52249, 116521, 11851, 5262, 26289, 48960, 29999, 94679, 33367, 125032, 72126, + 8676, 211498, 44721, 235091, 940, 97176, 26565, 5948, 20736, 10278, 50485, 12407, 11823, 41971, 135546, 103878, + 3020, 21249, 253851, 97728, 16476, 7536, 49750, 6746, 12340, 94756, 71789, 16549, 152600, 40488, 30681, 120494, + 97416, 90981, 75736, 36235, 3703, 36522, 4051, 90148, 25744, 143233, 74114, 50674, 66826, 186399, 55544, 63905, + 16245, 17811, 43885, 57562, 16876, 12660, 11009, 92234, 46446, 722, 137637, 44043, 84798, 27042, 5314, 21312, + 74227, 10917, 92213, 211986, 176020, 52818, 44610, 174541, 45192, 85977, 66236, 78509, 61955, 14788, 256627, 12032, + 75496, 83996, 101534, 82235, 23946, 104883, 3183, 12850, 23626, 57697, 3225, 16042, 40372, 9541, 108356, 126495, + 26036, 101893, 24582, 39880, 6149, 95087, 46546, 59092, 11822, 168583, 82335, 18729, 30582, 146256, 6074, 39807, + 16541, 11986, 80148, 61456, 41914, 80721, 142480, 21004, 82385, 83655, 47670, 4769, 232823, 41862, 437909, 4116, + 40921, 59664, 133104, 38104, 80773, 101843, 38426, 90874, 14930, 55522, 12793, 23708, 3631, 8582, 3112, 415975, + 7517, 106586, 112390, 31555, 39619, 56075, 6299, 30930, 4348, 38188, 23437, 11888, 36180, 29057, 78844, 52556, + 126106, 24776, 65214, 92664, 138939, 39642, 153427, 17494, 62611, 31501, 49371, 27056, 1477, 16503, 156270, 23995, + 113512, 205238, 84709, 77316, 47321, 67623, 42436, 36548, 25052, 10369, 19122, 181758, 14546, 6743, 124696, 124095, + 118881, 58058, 29158, 24211, 29060, 38102, 144694, 42736, 23589, 64142, 131963, 43763, 128322, 42128, 13330, 35824, + 36795, 108915, 43897, 15657, 18401, 58900, 14806, 118110, 137921, 30097, 47026, 4142, 104699, 4185, 87711, 85997, + 267929, 153261, 228359, 20660, 36194, 54339, 43073, 5692, 172791, 8213, 26146, 5686, 18113, 28694, 17786, 77352, + 4766, 1852, 168140, 76409, 188215, 2179, 198971, 53244, 30083, 37124, 32195, 48123, 332586, 62934, 88005, 15880, + 94089, 68377, 87929, 133891, 5805, 11217, 130365, 104237, 77909, 44881, 6260, 14391, 22194, 42271, 49170, 261884, + 68234, 79361, 14141, 107542, 154976, 30425, 14602, 73251, 43220, 47978, 4990, 19457, 40660, 21608, 104477, 3582, + 70001, 38324, 113052, 201756, 184893, 168071, 1921, 43965, 138095, 106316, 100248, 3825, 11128, 115487, 18833, 101956, + 103010, 11802, 30806, 12615, 22663, 5074, 17663, 19426, 5108, 141373, 42930, 183720, 212615, 34077, 17051, 59686, + 8485, 22524, 8691, 78244, 5565, 124298, 80099, 217111, 49222, 20498, 66793, 529503, 54614, 4186, 75927, 40419, + 26530, 57883, 5327, 44876, 42639, 19706, 60003, 74433, 16319, 1599, 26748, 132682, 55062, 6955, 63969, 38461, + 152662, 43166, 12979, 233, 105595, 18768, 75949, 56729, 21114, 135004, 23948, 42101, 22216, 320051, 21698, 23811, + 10294, 826, 29297, 123324, 68158, 26264, 158791, 95573, 10436, 191222, 9993, 12298, 86950, 123108, 20858, 320830, + 7206, 182464, 11757, 81755, 62115, 101508, 64002, 77412, 3977, 47597, 13370, 37935, 3657, 20526, 27967, 53371, + 59874, 220318, 6962, 46594, 456, 88484, 47078, 50207, 118617, 53233, 59503, 10872, 18156, 12054, 11152, 77825, + 106663, 95945, 18093, 1644, 112229, 13310, 264515, 9070, 80992, 55835, 162270, 15035, 17442, 111426, 61158, 71574, + 8217, 10224, 82739, 45979, 55551, 15768, 183976, 64417, 5133, 165269, 38884, 710, 34344, 57175, 27784, 31790, + 251927, 69733, 22676, 125462, 51153, 73708, 53147, 16750, 39364, 77866, 28089, 2461, 201321, 1010, 4858, 82482, + 7816, 123565, 3065, 124019, 66803, 1441, 2105, 147722, 23057, 2238, 2465, 109286, 156724, 37091, 60022, 193667, + 145664, 52352, 15876, 275211, 14276, 101280, 2609, 1583, 95705, 18345, 47036, 168953, 979, 1876, 17511, 99269, + 2796, 7756, 45223, 209272, 6875, 13501, 19972, 3039, 13429, 88164, 40587, 48347, 212525, 23645, 107978, 39326, + 50602, 71127, 74343, 5725, 26276, 95436, 69705, 115587, 28284, 114395, 42685, 63375, 3424, 66319, 70412, 12400, + 19465, 68740, 40524, 83167, 52397, 11466, 284475, 42974, 46963, 1472, 38647, 63800, 31420, 5787, 41395, 21919, + 51399, 116728, 34891, 76404, 206476, 116758, 145291, 39684, 92317, 182072, 3771, 102030, 48851, 66664, 589915, 408336, + 637, 73775, 252578, 42675, 100820, 82225, 43433, 158753, 83349, 100070, 42160, 13599, 10317, 42154, 91152, 132379, + 60227, 237752, 99758, 270587, 21972, 37249, 93833, 30614, 6908, 73035, 2132, 42490, 282439, 134241, 47775, 14940, + 32857, 14647, 284209, 112665, 224767, 50372, 44452, 85605, 95629, 194385, 241460, 28172, 83882, 169721, 7505, 79753, + 42106, 57611, 175719, 33441, 87338, 1110, 81138, 125252, 69757, 29461, 19692, 61492, 29840, 67754, 2077, 4100, + 68709, 14021, 73781, 501, 37665, 25322, 62392, 106943, 45244, 60281, 4687, 224380, 114577, 31964, 38842, 6435, + 49188, 3526, 19117, 97765, 175943, 38531, 30032, 27448, 54009, 25775, 5789, 30779, 186746, 7022, 85999, 42272, + 106158, 428476, 5637, 191524, 70168, 172347, 5932, 69204, 3358, 12019, 9877, 55515, 234002, 64516, 10898, 36793, + 50555, 139948, 27732, 39934, 9221, 14752, 203772, 22375, 129338, 7278, 3245, 117137, 9562, 29787, 9323, 102610, + 20118, 12371, 39558, 8236, 32923, 70519, 19395, 17047, 78479, 97791, 16130, 89463, 118280, 22050, 37024, 89641, + 65752, 52765, 71810, 108442, 4977, 44545, 69952, 19070, 10474, 23484, 18908, 21788, 102174, 82995, 4717, 97497, + 60947, 27112, 3005, 36385, 129006, 15642, 16432, 25512, 10570, 43825, 108125, 73183, 83451, 75116, 95227, 44522, + 8598, 15990, 22094, 1787, 8078, 14629, 33317, 78398, 159367, 125840, 12669, 67807, 123785, 64349, 37447, 22738, + 80438, 10768, 15104, 98321, 16742, 22746, 11237, 41146, 5905, 13127, 47595, 35425, 5281, 12465, 54216, 37342, + 181513, 107706, 142857, 10267, 42402, 87816, 7247, 28472, 6977, 228601, 32966, 2076, 163136, 1044, 31531, 56439, + 93179, 59512, 313709, 36615, 42191, 80611, 121959, 23989, 14968, 38253, 2001, 194381, 50421, 88797, 45161, 11328, + 112401, 85555, 7543, 130762, 105440, 130853, 18911, 12575, 33456, 139214, 29318, 62849, 57347, 85786, 171698, 27142, + 121611, 1138, 80752, 54791, 4221, 77844, 47524, 140582, 94954, 18043, 313972, 120855, 36517, 63651, 18688, 30456, + 24046, 74888, 58460, 70311, 27796, 50699, 84011, 20285, 6255, 200303, 74910, 57724, 33394, 16958, 104867, 22244, + 72990, 7001, 57825, 20114, 135408, 43742, 54632, 30820, 116627, 34162, 194876, 27214, 1233, 2542, 89114, 33428, + 57874, 24432, 73244, 13342, 25654, 36812, 42775, 137065, 95419, 3034, 109640, 69998, 68156, 15470, 40930, 43371, + 401399, 12617, 9352, 26808, 313338, 55155, 36975, 17890, 55208, 52186, 226992, 16324, 45573, 22248, 14465, 65809, + 93124, 76501, 130378, 8533, 119251, 28580, 43089, 52170, 47200, 44870, 76202, 54398, 38196, 30768, 33917, 36165, + 11909, 25697, 353546, 54562, 130667, 372979, 149630, 9125, 45391, 144756, 57973, 19599, 73904, 2645, 258411, 25268, + 64964, 61348, 128044, 132867, 167846, 141414, 52489, 108908, 4137, 41282, 39617, 33797, 115606, 493331, 11554, 30049, + 52036, 71190, 77943, 31602, 62214, 2239, 12602, 19146, 7969, 18428, 131922, 852, 160925, 6259, 23383, 165390, + 7187, 12808, 189017, 142381, 1132, 41841, 18701, 29043, 134835, 99, 15791, 9068, 40309, 98444, 127461, 27332, + 73195, 112064, 5097, 39453, 64494, 72450, 24011, 7695, 80472, 162172, 41084, 177912, 444841, 55640, 51858, 96982, + 61111, 3710, 41637, 100576, 26500, 116386, 75146, 6890, 45323, 42246, 54944, 26323, 40743, 9228, 36712, 134278, + 53625, 8230, 40754, 28377, 52797, 241804, 26552, 261558, 22659, 53109, 11974, 49532, 15631, 300307, 18096, 92, + 29739, 107565, 55632, 22306, 36706, 120177, 123369, 52145, 28841, 81811, 2967, 6374, 39147, 48191, 58003, 205210, + 102836, 34591, 52967, 14473, 26794, 120917, 61270, 53041, 10536, 41117, 108058, 363097, 14845, 27166, 219697, 10564, + 87305, 3185, 20437, 15052, 45874, 8849, 48484, 35021, 12241, 1310, 87883, 136400, 127587, 39626, 11562, 40667, + 83833, 19748, 78478, 3152, 57183, 48701, 22123, 86751, 79722, 116436, 5100, 237306, 30844, 5999, 191075, 65796, + 41304, 91788, 24098, 149740, 84655, 17286, 186934, 44080, 20825, 86063, 13887, 29758, 92500, 168321, 114890, 64965, + 3722, 47787, 198506, 43466, 25655, 5407, 13240, 101325, 27811, 1251, 36859, 37980, 10157, 135970, 13500, 104703, + 81634, 106088, 4011, 23578, 31362, 89054, 38594, 186633, 34088, 336505, 15479, 9229, 92487, 49521, 16527, 53016, + 70123, 18389, 75248, 16479, 22190, 140271, 80094, 10382, 185100, 9051, 71918, 40905, 72658, 25068, 29185, 15832, + 139035, 62499, 80063, 154077, 192523, 94489, 96542, 73906, 88241, 73266, 64690, 151133, 2078, 76322, 24201, 11290, + 230490, 211312, 36153, 103, 44528, 9539, 36954, 4068, 85638, 788, 127939, 83761, 100198, 46098, 285530, 41414, + 22088, 50928, 15371, 56983, 29982, 53638, 112051, 38452, 291233, 156894, 16748, 48674, 241062, 61341, 921, 24898, + 13865, 8071, 235453, 118716, 4445, 177183, 19708, 21189, 137791, 14558, 145674, 60895, 37835, 46685, 36314, 3038, + 107218, 25232, 61381, 204828, 31726, 43310, 143414, 32798, 19718, 11254, 127092, 88407, 38234, 37863, 143492, 19751, + 72528, 6573, 60076, 8937, 23046, 86273, 12926, 43848, 19177, 134938, 18033, 102451, 66695, 155909, 57463, 34776, + 5109, 25338, 58091, 2353, 17251, 3298, 36065, 271, 28077, 50946, 7379, 25203, 5617, 135531, 5335, 33853, + 21554, 1514, 322875, 85207, 47839, 81931, 162589, 73080, 72425, 41666, 2205, 107217, 133825, 13839, 41951, 21025, + 1486, 3869, 68800, 11775, 73065, 13658, 35920, 41391, 181275, 74450, 84216, 8800, 141508, 82895, 22465, 8542, + 21768, 13000, 8240, 5971, 62971, 23798, 245635, 47169, 63082, 25334, 10884, 141547, 2512, 44744, 8650, 42190, + 34200, 155357, 119815, 26059, 9904, 121764, 22752, 11476, 120309, 20694, 128696, 2930, 6392, 69057, 11264, 13880, + 91243, 58841, 35178, 12534, 68416, 70963, 20428, 809, 268253, 4190, 31257, 66625, 41199, 52789, 24931, 112688, + 116757, 6355, 182513, 90872, 138551, 2904, 50394, 19487, 185526, 10604, 30792, 10910, 41246, 165026, 42224, 113107, + 28986, 50164, 19196, 60791, 4093, 348976, 132032, 123165, 19057, 48820, 65522, 51163, 17295, 95175, 4484, 1308, + 4148, 239196, 12160, 143978, 245766, 14112, 90855, 4531, 122360, 139905, 44798, 12441, 35356, 12686, 74598, 10815, + 112075, 44675, 49095, 131228, 20301, 124634, 94533, 9419, 75441, 56741, 243625, 2690, 10998, 35598, 155583, 1231, + 7977, 46730, 242597, 113005, 19769, 157864, 288814, 15932, 62922, 81227, 227889, 46275, 937, 11294, 103072, 3033, + 63547, 8861, 201131, 23991, 100196, 49148, 38402, 10013, 26427, 1584, 47267, 32310, 157820, 36321, 1694, 31226, + 20983, 78184, 30061, 128167, 236696, 33413, 66741, 24836, 22935, 47910, 35635, 2678, 8140, 12435, 14911, 265634, + 90315, 43108, 109685, 72546, 156004, 8866, 37002, 25727, 47204, 51301, 8652, 23492, 140973, 93561, 41906, 95949, + 7726, 23391, 15506, 42212, 45097, 63870, 38047, 35887, 52725, 285299, 46733, 43623, 22636, 21753, 30231, 2194, + 23436, 125735, 107590, 55499, 257282, 66930, 21546, 157729, 105247, 1991, 14483, 140680, 522, 45784, 342095, 166370, + 88389, 122072, 21358, 24129, 216031, 54089, 2437, 107595, 202204, 163607, 26303, 41895, 46812, 40542, 78707, 2821, + 211666, 62835, 14082, 13734, 19693, 1603, 38978, 130765, 68828, 24577, 173255, 173341, 81691, 26679, 29930, 3426, + 45925, 32463, 13450, 24567, 11256, 19340, 65455, 7397, 30292, 9441, 111484, 57305, 372, 668, 19439, 90961, + 5236, 16883, 27753, 170633, 167826, 153144, 144689, 113366, 88328, 50765, 15108, 221902, 232776, 9372, 46450, 174146, + 151611, 40736, 105766, 1040, 5360, 65342, 48072, 55757, 82104, 206605, 5190, 78870, 18841, 118613, 78897, 18174, + 80393, 27669, 110040, 158621, 25465, 12309, 35524, 25100, 18285, 9967, 2817, 4292, 20320, 37594, 50630, 17986, + 72377, 87322, 63433, 19730, 31730, 51337, 57653, 20420, 33160, 147371, 43603, 49870, 45803, 188912, 85055, 11824, + 38715, 173010, 66391, 15353, 27705, 31430, 7938, 120390, 37379, 123726, 25452, 345, 24163, 78121, 86371, 105015, + 18360, 52480, 28116, 10182, 103586, 12625, 43431, 22706, 4015, 110996, 23635, 211386, 32305, 9669, 5607, 79602, + 269494, 361, 52000, 75409, 91252, 16133, 217163, 25649, 20080, 40640, 108653, 52740, 36567, 24900, 6769, 93887, + 54650, 108271, 21897, 49233, 7797, 97128, 54633, 281648, 57073, 36934, 16265, 167589, 12650, 209086, 62181, 5542, + 31164, 122349, 81815, 67651, 42209, 38682, 31383, 122219, 6375, 11705, 72211, 22777, 261663, 21772, 69350, 85211, + 105528, 83301, 31254, 16229, 81661, 5138, 1814, 117287, 106002, 97442, 104335, 3000, 2800, 69084, 40361, 68511, + 5375, 11262, 76099, 146464, 17247, 106291, 8526, 92034, 43151, 22579, 624, 893, 4442, 9159, 34683, 63801, + 15727, 54296, 36030, 7741, 194619, 26966, 57386, 521, 100855, 29101, 4249, 27495, 144898, 122045, 157745, 29287, + 62320, 13800, 162790, 170925, 78465, 1720, 58206, 24580, 39929, 46731, 7089, 342751, 16454, 254942, 25844, 55945, + 1967, 64379, 6774, 82424, 28311, 43620, 50135, 58126, 61363, 144665, 15325, 125247, 17219, 113505, 10215, 205466, + 9395, 52783, 670, 169135, 8745, 37048, 58689, 91650, 121445, 22644, 27467, 23132, 76939, 233859, 30141, 24917, + 80385, 41981, 8292, 35986, 162380, 57998, 43320, 212379, 22009, 3420, 17253, 49172, 54191, 105827, 25802, 3604, + 44248, 112875, 6127, 145960, 16299, 26569, 47356, 14611, 122830, 30067, 21003, 192364, 48151, 121965, 327180, 5291, + 74429, 11440, 101228, 65667, 78291, 16492, 12720, 3052, 64755, 138800, 114706, 157348, 14238, 155607, 17452, 62457, + 44966, 41313, 98226, 78628, 2511, 99734, 74253, 133560, 17712, 1897, 74082, 2056, 67954, 146843, 392522, 79571, + 93583, 59314, 13047, 43084, 829, 117157, 13262, 38703, 105899, 54574, 104172, 22161, 49935, 96314, 98235, 13239, + 84750, 53580, 34154, 114889, 11591, 72967, 66707, 104862, 33185, 16902, 22355, 42766, 85447, 36865, 165090, 84531, + 42717, 343264, 93849, 81105, 27409, 214283, 113392, 12987, 208542, 45776, 2947, 83062, 28965, 25376, 24971, 20612, + 62052, 26121, 83563, 52492, 52525, 3766, 104113, 40486, 5597, 125, 87057, 6525, 25694, 6096, 199883, 183361, + 65594, 40734, 5880, 182733, 16343, 16696, 86236, 796, 63224, 7501, 54031, 26465, 276188, 66868, 2954, 84925, + 12475, 2514, 223628, 75284, 9331, 78770, 22694, 89261, 127507, 56323, 111184, 138073, 38522, 10128, 31041, 55066, + 57287, 41322, 157136, 126272, 24128, 75696, 37221, 12395, 133161, 60386, 24760, 6499, 79723, 24499, 76440, 34317, + 105548, 60338, 146011, 210310, 133695, 189639, 390311, 10417, 48917, 77547, 25558, 125695, 27558, 50249, 50758, 54053, + 43278, 12651, 41946, 33000, 46520, 10764, 46950, 144684, 13778, 1185, 25117, 155135, 141954, 96252, 124109, 34349, + 110785, 24351, 73685, 9493, 83366, 25352, 100253, 91513, 17715, 18369, 120888, 58948, 46317, 59607, 74045, 40939, + 105763, 18522, 3886, 43064, 66298, 18918, 33778, 108305, 147013, 68203, 169050, 212793, 41086, 17383, 9620, 80428, + 94180, 46324, 90898, 384293, 16478, 2162, 57856, 4565, 220447, 72458, 27818, 12844, 44611, 11585, 8997, 5614, + 730, 93897, 76702, 124308, 19722, 10289, 81775, 44327, 78975, 156207, 11289, 31827, 117889, 212382, 163177, 125496, + 125643, 46366, 29130, 40665, 26254, 99947, 10510, 84091, 16574, 120313, 104829, 138305, 18480, 16829, 8176, 17121, + 65006, 22646, 16279, 64878, 15806, 31922, 5544, 11868, 38549, 32569, 219914, 15814, 246418, 24003, 289255, 313945, + 46052, 157270, 10005, 11234, 36056, 153288, 35135, 63031, 8440, 1275, 43404, 298391, 34984, 44385, 45317, 14880, + 30170, 5875, 12546, 45344, 3163, 4914, 1779, 66305, 59800, 72227, 6487, 36265, 4458, 73948, 78254, 47797, + 115442, 39130, 71796, 11449, 4283, 30975, 181777, 25982, 41970, 19821, 110322, 41466, 33507, 69754, 31950, 489525, + 104078, 4158, 69239, 85214, 1653, 56217, 34223, 9944, 22, 35473, 131427, 25058, 121158, 166086, 254762, 9745, + 276486, 4120, 33379, 30250, 3655, 10419, 70458, 24518, 6338, 80002, 3419, 4757, 24048, 2139, 938, 210466, + 133421, 8823, 35575, 1574, 23641, 94423, 17694, 113822, 2161, 40833, 49449, 5625, 24422, 131600, 516, 97149, + 36006, 24910, 111249, 104788, 8086, 63142, 17790, 49461, 10675, 30015, 25712, 12492, 181474, 43374, 19331, 39246, + 12307, 170327, 251360, 85956, 29514, 4826, 115208, 41680, 59143, 108007, 189711, 48650, 14729, 28727, 61638, 233522, + 52509, 9807, 83278, 43091, 87128, 205247, 225046, 135671, 122470, 46278, 118683, 70557, 19446, 17394, 5920, 32166, + 80852, 8067, 86203, 410557, 33314, 48904, 140515, 16642, 24573, 28653, 11481, 199433, 119864, 5958, 55298, 63357, + 14237, 29162, 73299, 12246, 9652, 101149, 98618, 9611, 57779, 9592, 43988, 1014, 6612, 44197, 23629, 95201, + 51851, 69205, 94416, 985, 15284, 36462, 143673, 5555, 98871, 71794, 26730, 9761, 90581, 198325, 133604, 57541, + 124466, 1970, 23017, 51130, 156831, 80242, 24852, 47760, 21190, 76629, 13862, 175502, 22015, 38086, 18855, 52006, + 71380, 1872, 6332, 88866, 161906, 10811, 92768, 111004, 87247, 58129, 1244, 4815, 69201, 134735, 125070, 78458, + 18392, 146150, 4886, 66816, 17908, 31646, 6778, 69889, 108470, 70270, 87563, 7366, 72962, 13622, 24210, 28614, + 40719, 40963, 61900, 8988, 14338, 191234, 63447, 35774, 17911, 12734, 257831, 101714, 95260, 78005, 17582, 88301, + 43339, 8921, 3918, 82471, 20610, 84864, 253574, 21526, 78916, 58506, 1435, 433170, 20710, 147863, 28259, 4548, + 72451, 2742, 1878, 156795, 11315, 75234, 32104, 46510, 31448, 15513, 60382, 37929, 17263, 142357, 16994, 34753, + 58853, 23100, 69236, 5182, 178878, 2545, 32116, 152276, 48111, 71595, 81171, 93964, 116002, 57412, 4245, 19877, + 45497, 156962, 37019, 27777, 80506, 87318, 64770, 13895, 82605, 511, 8366, 87800, 85880, 42605, 80454, 41603, + 36300, 27404, 35498, 82743, 121755, 128166, 1871, 81847, 25215, 5995, 60337, 177660, 36118, 54831, 138197, 126290, + 301929, 182977, 7852, 68390, 88728, 111588, 58683, 115820, 405223, 74730, 36671, 37592, 276136, 25471, 6538, 61648, + 553, 89532, 101905, 27590, 34704, 25370, 42539, 28362, 212438, 98439, 26905, 4804, 49970, 60393, 7138, 33248, + 78329, 11555, 51340, 1796, 922, 17391, 17952, 3534, 20711, 34703, 17977, 21148, 25036, 13075, 6201, 46397, + 257130, 73731, 213188, 107033, 38295, 56744, 112539, 122324, 145369, 82762, 25343, 6279, 18128, 140444, 35989, 4474, + 15385, 14399, 63732, 21041, 30829, 98387, 31090, 23985, 55656, 40284, 132675, 4230, 48345, 64072, 15739, 73537, + 8012, 23754, 107906, 9060, 3561, 183232, 19339, 47011, 28004, 85175, 46305, 37773, 122041, 123221, 100918, 79448, + 192900, 143005, 63829, 123166, 58338, 37779, 33298, 109415, 112508, 115553, 68473, 184384, 41085, 82184, 61692, 70292, + 29976, 115765, 134590, 89398, 87040, 39038, 8330, 21348, 47117, 350, 39878, 167810, 23905, 28058, 42971, 15124, + 4336, 12612, 11230, 18966, 92061, 59408, 12185, 128370, 138880, 27612, 27335, 49677, 97407, 61794, 36678, 60848, + 42083, 7518, 89496, 57735, 172121, 74451, 85960, 144252, 6256, 110791, 28888, 21551, 25192, 5388, 80308, 1657, + 172671, 42290, 48, 195157, 5, 36476, 3307, 89171, 93568, 157177, 7894, 66766, 1420, 14058, 167637, 30649, + 12677, 121794, 69021, 13231, 31605, 230408, 111760, 67720, 56743, 31038, 52660, 61746, 40620, 81659, 12864, 14180, + 6015, 215868, 90084, 141993, 78415, 116175, 52441, 7100, 231077, 112914, 39586, 51204, 31298, 52736, 64704, 3584, + 80026, 74802, 13972, 215480, 13902, 93110, 20076, 42335, 19048, 41860, 36983, 169990, 24924, 34272, 70434, 20851, + 170586, 12670, 115383, 39041, 32955, 71225, 30362, 30667, 176119, 19860, 10392, 48818, 87859, 7042, 111918, 36751, + 36731, 38144, 156426, 19519, 6773, 997, 29515, 53787, 27711, 72672, 159500, 51584, 24658, 40226, 1920, 70951, + 26475, 36713, 58252, 424939, 115216, 30508, 35252, 34310, 133207, 17198, 8490, 5530, 93250, 121485, 122775, 66308, + 95820, 59227, 108938, 53397, 88522, 6280, 46904, 14869, 8317, 25024, 14413, 98196, 5714, 54882, 8596, 43570, + 124047, 5657, 302052, 35, 55219, 105769, 57203, 241055, 86860, 6086, 121752, 2426, 19677, 79643, 9137, 36087, + 23961, 35741, 73879, 95404, 22928, 151374, 193172, 7078, 162209, 225418, 143148, 102355, 8904, 2279, 71928, 20953, + 225992, 15275, 49711, 9805, 359835, 337011, 5747, 97478, 56084, 49629, 13712, 164652, 96201, 13261, 69730, 256157, + 29392, 154079, 57395, 17417, 96558, 76159, 89103, 154068, 86071, 1481, 55845, 81677, 93643, 5966, 26830, 128209, + 55114, 215629, 46997, 96220, 13347, 6748, 42089, 26362, 8183, 87721, 60802, 344, 95129, 74756, 31533, 58592, + 82012, 86256, 21385, 21021, 2017, 92952, 1400, 17103, 123336, 48961, 24557, 31513, 34219, 94330, 102993, 33899, + 115554, 104210, 45821, 24373, 157159, 41476, 17357, 64836, 47747, 20243, 42746, 100702, 101684, 123185, 46219, 68593, + 41008, 135956, 132526, 17386, 18735, 62966, 27255, 23471, 193781, 89150, 2616, 57853, 104151, 97883, 5292, 181287, + 226906, 114560, 56557, 82841, 7552, 61282, 26131, 1854, 179874, 74127, 60152, 17376, 124113, 21668, 38821, 14260, + 31159, 29704, 82096, 204953, 21162, 64569, 11540, 37899, 44010, 79498, 50023, 39667, 14771, 235800, 15254, 382134, + 51268, 23240, 7289, 52385, 166128, 26710, 362, 239, 31382, 28658, 33227, 60344, 73124, 109004, 15966, 14103, + 77438, 65958, 13394, 9762, 92830, 27896, 85690, 1773, 205709, 14225, 11061, 49451, 12113, 15690, 22771, 34399, + 1292, 19355, 16900, 29634, 38937, 103120, 45588, 32502, 13114, 67382, 21201, 255605, 1334, 78137, 14683, 11960, + 2118, 39992, 111625, 158751, 15597, 47752, 3544, 123201, 69581, 8045, 25346, 232034, 14449, 70422, 61526, 29243, + 21934, 152079, 39735, 29007, 76618, 18195, 153, 27890, 48728, 123899, 68311, 48831, 67038, 51326, 21747, 8692, + 14967, 4922, 69709, 15204, 51495, 12889, 28173, 172579, 24243, 12645, 53481, 33706, 87736, 191077, 102314, 17799, + 147249, 101629, 161049, 4798, 26720, 45298, 89381, 76472, 11119, 5522, 59583, 86712, 46063, 85661, 57137, 135132, + 43749, 6257, 4316, 44510, 5843, 232177, 211804, 97913, 44147, 11778, 5876, 129172, 152629, 43931, 5404, 124003, + 133428, 97102, 297704, 57882, 65703, 24717, 67411, 23646, 14269, 13007, 172728, 4591, 45604, 57195, 12344, 102301, + 57982, 61997, 95626, 254590, 28672, 129544, 121196, 14934, 55616, 30754, 102467, 25644, 45957, 41766, 105011, 59359, + 8438, 80296, 37663, 282854, 95433, 8167, 10263, 65749, 37698, 56099, 237283, 25218, 220862, 42641, 90021, 45007, + 132034, 144203, 26155, 333670, 39456, 63723, 27049, 39050, 61870, 153573, 33337, 28349, 4161, 99154, 19089, 5689, + 26501, 109719, 53252, 38261, 73560, 103696, 77848, 61508, 56418, 8146, 14836, 30856, 9845, 7946, 131232, 127176, + 4654, 201895, 63780, 158964, 20916, 91453, 54086, 43072, 10456, 54618, 169463, 46325, 88920, 26447, 165915, 26064, + 119358, 76658, 11753, 55625, 9015, 143112, 11520, 269278, 65931, 121236, 179251, 9829, 96507, 5826, 140198, 247937, + 48029, 31061, 53953, 19018, 38534, 95073, 117331, 37745, 21676, 22041, 2439, 21017, 109081, 12120, 58943, 64119, + 43078, 113509, 30640, 37723, 34943, 350151, 79862, 143849, 25089, 16375, 77573, 16298, 6131, 168435, 40662, 50028, + 28766, 133195, 55258, 45544, 23665, 5135, 54199, 18072, 5477, 69501, 106176, 37245, 10255, 33562, 9039, 7013, + 16695, 29965, 30540, 106269, 67, 100273, 20386, 1936, 45778, 12653, 4617, 22300, 42443, 22525, 113152, 24837, + 42770, 66959, 15725, 52734, 29534, 17022, 9388, 334890, 23733, 206600, 71568, 50746, 100513, 18013, 100834, 84319, + 62617, 6595, 63101, 185162, 42630, 76641, 44106, 8165, 48746, 35407, 72152, 6974, 14191, 19580, 30414, 39009, + 43753, 63797, 66647, 169610, 50295, 55790, 16670, 533, 26007, 9475, 35597, 110160, 8792, 8536, 3652, 9896, + 57243, 120506, 37648, 15821, 43119, 131495, 35035, 35955, 54725, 31031, 169396, 210089, 164253, 337858, 11412, 77625, + 58250, 28866, 7134, 37720, 112304, 27676, 81211, 65246, 131796, 9378, 94506, 130705, 25165, 11403, 69938, 129091, + 4651, 244698, 53305, 17335, 3188, 92793, 26039, 26725, 24831, 59363, 5282, 15758, 47748, 53877, 45181, 5916, + 3705, 107447, 7016, 107200, 19540, 12998, 120359, 9387, 13211, 7466, 190758, 9392, 102095, 49540, 128418, 188973, + 5593, 50901, 14566, 196318, 18699, 54825, 76278, 15395, 23666, 36000, 88985, 17589, 32005, 23087, 139968, 578970, + 117571, 145460, 7486, 3620, 33541, 117754, 26398, 27210, 60584, 18889, 20981, 1633, 74573, 38616, 14563, 6638, + 86311, 86421, 25557, 42564, 99443, 36130, 97953, 1966, 25172, 83868, 86024, 136991, 27222, 67124, 48935, 48573, + 168938, 36866, 83414, 114109, 7143, 95340, 16828, 116402, 11853, 106392, 138070, 12698, 53560, 1736, 35550, 51146, + 18834, 22574, 37375, 59652, 19960, 32431, 133520, 38384, 86522, 43137, 73914, 52482, 28217, 63002, 68351, 25347, + 53266, 74606, 37238, 64254, 117700, 2458, 69113, 78847, 72989, 4146, 51993, 68944, 34323, 36106, 26991, 208463, + 18721, 68637, 46037, 28177, 66450, 253789, 306984, 98512, 34346, 8717, 62376, 80238, 74056, 18246, 43446, 108106, + 47217, 76264, 2682, 95684, 202002, 20293, 22279, 7215, 46269, 5998, 50165, 12049, 9429, 33348, 73125, 27380, + 68582, 110884, 139796, 3350, 75458, 44810, 10607, 55791, 37823, 11401, 91589, 124148, 82843, 54499, 20716, 21192, + 96652, 231365, 33208, 41522, 32549, 31981, 17453, 40828, 145144, 39135, 25049, 9457, 27958, 103347, 32810, 43385, + 19820, 53369, 126180, 23951, 158086, 100187, 144947, 109980, 31955, 45450, 139714, 69089, 201406, 73593, 53985, 61675, + 135379, 20004, 20095, 20957, 31207, 58416, 82105, 5163, 192545, 15375, 4004, 14708, 12950, 3007, 33958, 201104, + 51704, 2284, 38239, 215421, 9094, 170678, 68181, 18112, 248263, 264771, 5839, 65767, 76147, 15661, 13266, 123439, + 64028, 57174, 54342, 51589, 110009, 117919, 35421, 20125, 79407, 23467, 26562, 86760, 89345, 56253, 2635, 17366, + 99284, 45433, 65377, 5392, 223492, 45675, 94215, 41399, 47966, 42373, 88171, 10577, 26848, 109297, 198937, 27318, + 15359, 54014, 189688, 14526, 201137, 11181, 412, 4944, 2861, 25417, 41460, 9506, 110507, 60409, 42693, 128247, + 71231, 35793, 106673, 70206, 72297, 6817, 45319, 20585, 31851, 34828, 8363, 13868, 118777, 52262, 102674, 38674, + 71039, 6463, 77753, 62309, 151051, 2673, 41364, 138637, 240855, 165918, 128697, 37821, 16333, 64502, 32568, 39824, + 50766, 20504, 14159, 65654, 14727, 29996, 6383, 42332, 7939, 14308, 8137, 87581, 4149, 88311, 165279, 7060, + 80908, 49103, 28053, 140076, 418780, 65632, 172843, 34187, 88378, 29997, 545, 51172, 59276, 68688, 2146, 123301, + 1327, 93829, 16449, 152910, 7284, 87749, 23351, 3595, 38576, 26223, 23401, 96146, 79814, 32323, 172636, 114120, + 65820, 86539, 208708, 76711, 42199, 188268, 14011, 15148, 84860, 5832, 11441, 25143, 49574, 6953, 65399, 115759, + 62596, 134341, 14187, 18034, 12396, 16855, 108734, 25950, 70598, 14918, 2364, 136070, 40117, 153617, 71320, 115693, + 8648, 35391, 90995, 4541, 7994, 124212, 8041, 66856, 16836, 11191, 127474, 68712, 7630, 145631, 110314, 22066, + 14047, 1763, 167187, 174630, 359699, 151667, 4177, 41323, 106878, 1793, 50733, 69877, 525, 43923, 4828, 82891, + 29037, 171929, 324595, 65899, 28064, 91271, 65021, 37989, 13380, 31251, 2943, 407325, 11675, 18588, 38513, 8392, + 50669, 90077, 18495, 9450, 74216, 88387, 45547, 81293, 103539, 10795, 31036, 15610, 180314, 21332, 14365, 65897, + 27449, 55756, 149067, 98485, 56299, 40269, 32366, 5250, 172344, 23410, 92231, 18561, 19274, 44347, 122063, 8423, + 7301, 50878, 28326, 38202, 246099, 51058, 26017, 51056, 32043, 22795, 132375, 6943, 19422, 300889, 9098, 15040, + 36506, 86225, 57465, 49440, 129317, 12410, 39803, 48164, 6806, 32451, 82234, 132656, 30140, 15444, 60069, 3760, + 4614, 25897, 159, 28083, 46639, 99936, 101909, 30904, 66926, 173983, 34220, 17421, 932, 21450, 21043, 113018, + 86600, 42052, 132088, 128256, 6322, 29, 124274, 99295, 27847, 33188, 30130, 20462, 233103, 11629, 82802, 23282, + 10541, 47224, 44501, 68401, 39025, 9673, 4555, 49397, 34887, 30110, 111225, 33589, 3517, 74773, 153162, 143852, + 12972, 3530, 24385, 31076, 26220, 32573, 109457, 43144, 2031, 300519, 129953, 9238, 66561, 196911, 99752, 28368, + 115015, 174339, 10996, 8970, 48658, 26284, 10285, 694, 47596, 445399, 13275, 71937, 12714, 3528, 3654, 55811, + 33845, 269037, 118340, 11397, 3893, 231593, 42203, 2159, 16165, 12556, 48781, 72601, 35237, 68887, 18601, 17941, + 89983, 98696, 97413, 15006, 14769, 128317, 13607, 7426, 11962, 244737, 72660, 28493, 147224, 62322, 6982, 534, + 47018, 29199, 6388, 175620, 29977, 6018, 54235, 144119, 27979, 74453, 23072, 144966, 5552, 22680, 20439, 8839, + 82338, 13010, 108618, 484, 86023, 58829, 612076, 306318, 131368, 51558, 106387, 22146, 1218, 11904, 270917, 87286, + 24853, 275508, 76852, 5463, 237840, 82183, 83602, 44537, 132193, 104954, 15511, 29147, 15455, 20088, 13624, 153696, + 40873, 90309, 158996, 18756, 3668, 32089, 12462, 41486, 65351, 2207, 126590, 223126, 53388, 4077, 30070, 25994, + 15229, 66181, 63714, 79318, 59889, 160619, 15153, 12456, 272245, 42542, 24758, 47453, 47934, 65558, 28145, 13413, + 11858, 21191, 27021, 92095, 34347, 32570, 60556, 197324, 18038, 189522, 6812, 28247, 90853, 8634, 180039, 7329, + 86981, 148519, 9289, 4888, 300602, 74523, 9708, 49271, 19343, 63426, 74637, 174896, 114181, 28008, 26788, 8747, + 29362, 99695, 52578, 19976, 84921, 101587, 24035, 38033, 6095, 92547, 36135, 72547, 106059, 4207, 181943, 47905, + 79472, 24101, 58847, 20037, 38015, 46536, 22348, 109208, 1206, 45622, 37915, 26165, 48741, 7232, 1005, 4065, + 6208, 18528, 151721, 62784, 80000, 18897, 5854, 32564, 21916, 4912, 34596, 201975, 17423, 134488, 54506, 154143, + 6002, 72868, 7520, 36987, 108083, 112937, 75656, 32044, 24479, 15432, 40091, 58495, 34931, 9602, 16762, 2442, + 56661, 153348, 22812, 98811, 9511, 56184, 1409, 9718, 26995, 197, 49218, 167694, 100694, 44775, 53809, 6725, + 163853, 45609, 73, 98613, 35997, 111306, 20357, 36132, 81254, 14735, 26511, 23223, 58321, 51715, 70878, 210120, + 18919, 64042, 68936, 3280, 171890, 120028, 29382, 125765, 86877, 48327, 105142, 9969, 91341, 1198, 32748, 184452, + 74503, 188311, 34295, 291694, 70477, 184873, 41972, 33654, 53412, 208288, 120160, 47130, 7027, 83694, 30556, 99012, + 59281, 43841, 119720, 74947, 39892, 41874, 34808, 25853, 131302, 33590, 193671, 96415, 5864, 20797, 100412, 35136, + 15947, 13143, 28210, 28717, 61301, 5772, 22132, 12722, 67466, 83494, 128102, 176810, 162369, 37658, 11958, 19685, + 47956, 184482, 37093, 126586, 27874, 17054, 70824, 29354, 35624, 31284, 22117, 80846, 282324, 89289, 5812, 73290, + 21270, 10973, 15681, 37548, 111847, 5570, 82, 33861, 102548, 16344, 48551, 72401, 41482, 3442, 126293, 65750, + 30955, 205735, 76092, 105960, 116737, 54740, 25948, 8274, 28264, 111716, 110866, 252362, 8592, 112975, 10016, 89336, + 55458, 589, 60295, 9209, 22301, 101479, 32825, 64171, 75090, 106896, 21619, 26306, 29821, 30637, 132744, 10929, + 30697, 32956, 20307, 87608, 51709, 317395, 23678, 60949, 3041, 29345, 83581, 57276, 19208, 37802, 184528, 147377, + 8038, 29282, 6679, 77158, 24634, 288107, 38217, 32048, 30467, 20308, 11907, 18988, 87509, 32192, 89058, 11561, + 126428, 56272, 20368, 65338, 19389, 104074, 29008, 8049, 18814, 30607, 339, 30562, 152686, 25435, 16446, 78067, + 20701, 122568, 3475, 39655, 83474, 29081, 12004, 116638, 45832, 69977, 107214, 46129, 80891, 11087, 68939, 167121, + 105808, 42070, 117480, 33702, 11378, 25602, 124846, 110381, 153223, 6672, 47709, 80371, 120770, 144580, 11225, 22744, + 98186, 104427, 63765, 10261, 150633, 71276, 51714, 50281, 49838, 190522, 8987, 235791, 9141, 340331, 86198, 17567, + 12755, 210134, 6552, 101752, 30962, 11267, 2982, 8706, 5260, 70113, 110165, 127201, 74490, 93235, 145772, 35288, + 21256, 25014, 2694, 10842, 31678, 81199, 6436, 125138, 65062, 15308, 22318, 40788, 33326, 9604, 145295, 6946, + 289838, 90308, 57283, 326774, 187831, 2998, 288064, 53373, 20595, 188251, 90664, 58927, 89768, 31693, 379534, 13903, + 2805, 2413, 50298, 47087, 58535, 56981, 59799, 135588, 10844, 74455, 88188, 208971, 70085, 92510, 18541, 33920, + 12090, 24555, 11134, 23200, 2451, 6275, 135010, 5521, 138068, 6562, 161302, 44283, 98544, 214408, 65576, 50058, + 24461, 33687, 92862, 93762, 4511, 119896, 3685, 41519, 6754, 9529, 39394, 26959, 41684, 51727, 24271, 311732, + 28203, 45748, 149989, 111355, 3383, 25313, 20209, 43668, 65355, 32723, 38554, 67434, 82833, 42676, 66416, 25724, + 30161, 223, 65165, 45436, 83924, 12283, 5232, 29830, 234361, 377903, 56, 40022, 128424, 12472, 60521, 234629, + 28921, 22232, 168965, 36999, 222594, 73040, 24600, 9682, 33975, 51080, 29219, 59847, 125491, 30202, 38650, 136512, + 34069, 20533, 23257, 105963, 11508, 99917, 34237, 4124, 67464, 43359, 26791, 158522, 144226, 38792, 18892, 13958, + 41850, 71554, 19647, 61226, 98703, 124759, 9034, 30539, 34371, 467683, 65227, 93480, 7901, 29703, 82581, 17619, + 21254, 2734, 102235, 57361, 38398, 17196, 96566, 364971, 65651, 28491, 137653, 151200, 23549, 142281, 78664, 84647, + 53883, 34900, 46611, 40124, 213340, 36091, 30841, 52023, 123269, 197827, 78380, 73808, 12028, 41191, 45370, 7903, + 71764, 90561, 215513, 34771, 177701, 60585, 32517, 25415, 28758, 92364, 41752, 82386, 2623, 76984, 37173, 50064, + 68395, 4403, 43681, 98106, 11549, 37791, 53245, 104084, 15232, 60303, 28511, 10160, 68603, 161243, 108330, 25902, + 9660, 75510, 24031, 320844, 63116, 2614, 187946, 158900, 36079, 32819, 38425, 121867, 57093, 51652, 80423, 5684, + 31198, 161752, 36511, 146667, 20475, 4507, 19201, 30548, 48467, 78275, 5077, 22549, 89984, 50714, 17018, 66706, + 35619, 3531, 25020, 48835, 186847, 9463, 64248, 111387, 107469, 188636, 105137, 8703, 31389, 34009, 25409, 14207, + 43631, 12180, 14360, 89167, 73867, 33692, 74998, 119507, 41949, 87046, 115959, 6770, 68841, 6405, 81460, 142743, + 114250, 5132, 148038, 8544, 1605, 15540, 177509, 152453, 30564, 1453, 29815, 32099, 63403, 6534, 252, 11418, + 17588, 16946, 116226, 138792, 27680, 101291, 14339, 60744, 99533, 194699, 39891, 16377, 12641, 146345, 141037, 140957, + 70325, 42826, 21176, 19061, 50428, 51375, 12005, 144174, 73426, 76160, 46461, 129205, 78379, 40145, 25474, 133179, + 11855, 127230, 80781, 27661, 91651, 396, 88286, 34420, 72081, 94521, 46013, 15403, 91720, 88126, 549, 79964, + 60198, 187793, 125452, 14392, 15743, 83700, 1715, 206475, 12065, 22805, 39201, 63593, 83398, 13801, 99478, 25699, + 140046, 83985, 258911, 49800, 6761, 9435, 74133, 1389, 46598, 11934, 27897, 5386, 45900, 50232, 260248, 96335, + 5068, 33530, 49692, 65315, 886, 4019, 40151, 91916, 62448, 10628, 13597, 141884, 148968, 14083, 261394, 163080, + 37347, 1276, 8705, 22521, 19405, 57813, 55957, 200637, 9680, 15938, 42198, 23570, 15819, 166772, 12555, 37690, + 43496, 26735, 63396, 52654, 63370, 37031, 83006, 34067, 75667, 18077, 2332, 127143, 163700, 1741, 5704, 42945, + 37639, 34898, 146321, 65846, 3633, 51526, 134217, 9232, 22774, 1772, 66282, 48233, 34341, 21424, 13242, 67351, + 183131, 97579, 28584, 80530, 134335, 38834, 239665, 40278, 37200, 32081, 53185, 40538, 23915, 99449, 228969, 44407, + 7054, 17139, 112714, 101287, 14194, 810, 50801, 33269, 12970, 136397, 73876, 55691, 26438, 37462, 53441, 99617, + 13350, 42910, 71168, 42168, 285521, 78217, 93695, 78702, 25594, 30022, 14876, 15315, 8219, 34298, 97268, 51265, + 104410, 54835, 26232, 30943, 91039, 14205, 64948, 29544, 168804, 48486, 75782, 69559, 138480, 99517, 39422, 37873, + 149734, 38422, 8276, 41956, 15907, 166205, 43978, 154555, 33818, 28840, 72911, 38423, 61132, 39592, 4460, 37088, + 60082, 68918, 21965, 92108, 4622, 33094, 35917, 111849, 110187, 72295, 8713, 89755, 56736, 89742, 45364, 6790, + 13551, 41957, 48065, 8188, 73571, 62739, 53050, 144415, 3945, 24426, 98109, 2450, 73463, 37147, 78116, 9166, + 65498, 53281, 120479, 179965, 17758, 17052, 32430, 155198, 263266, 84633, 148996, 48061, 17593, 38254, 12219, 51478, + 2710, 95095, 20869, 29664, 27585, 13750, 47237, 181, 54469, 7569, 60566, 139036, 38200, 24063, 28235, 13774, + 45367, 230307, 83783, 80750, 63754, 95816, 43190, 122385, 28881, 144847, 15520, 94088, 3473, 3890, 113331, 76118, + 12791, 80924, 118713, 104097, 98287, 136037, 67781, 10689, 31895, 90630, 43079, 93377, 65787, 61758, 1438, 103534, + 4463, 1696, 4512, 120430, 94536, 3019, 34145, 75690, 24951, 53596, 58640, 11685, 36332, 6632, 11422, 26659, + 59901, 43634, 130651, 33557, 28803, 3908, 133680, 9899, 52130, 43287, 6912, 145132, 86403, 177137, 54381, 65649, + 7668, 54154, 97545, 91326, 181822, 89508, 11188, 2346, 74831, 83183, 79610, 47140, 18977, 54325, 82292, 130974, + 9850, 25539, 71074, 133045, 177206, 71768, 81956, 28358, 145485, 83131, 29163, 37533, 109798, 18435, 75184, 52995, + 7292, 6596, 2251, 2040, 31421, 19770, 177982, 2607, 26280, 108782, 69065, 324604, 77211, 91802, 37849, 30896, + 58511, 49307, 70, 135829, 12507, 6486, 51031, 9444, 127004, 180981, 46202, 95370, 11113, 65980, 7248, 1864, + 147, 52898, 183217, 22572, 8729, 6517, 4166, 36847, 56208, 124700, 29553, 6200, 43066, 1797, 193216, 150871, + 79926, 125256, 38154, 42479, 129937, 102831, 33444, 18931, 31345, 35792, 147516, 19534, 83947, 136739, 76241, 217709, + 39915, 177, 29612, 176661, 46146, 21703, 66186, 15852, 98763, 32284, 15029, 20993, 42566, 64132, 96065, 164496, + 1337, 178401, 214897, 22155, 13192, 119711, 143143, 16098, 18323, 17920, 25851, 94549, 105163, 90198, 141550, 124816, + 80570, 619, 144085, 9847, 117753, 16770, 4661, 55732, 16555, 15568, 31762, 116903, 72883, 28446, 93397, 75397, + 11077, 69803, 1471, 136227, 159438, 102246, 162403, 73442, 40764, 27766, 18455, 53335, 70933, 12129, 19140, 50321, + 83329, 51649, 28681, 50106, 26066, 38874, 69436, 77741, 12276, 9285, 17504, 16474, 72059, 83880, 8401, 45101, + 21655, 38252, 34797, 40053, 173836, 50540, 136368, 21585, 126713, 108126, 142751, 130247, 69454, 55138, 130179, 11656, + 153482, 162984, 158538, 204679, 91585, 26846, 8149, 101037, 70644, 67384, 153679, 898, 102558, 18887, 79015, 11681, + 110483, 73677, 144865, 17407, 6764, 28552, 369746, 32468, 127864, 203511, 3905, 45256, 190133, 1948, 85436, 54536, + 3961, 2931, 39613, 52497, 101798, 205435, 63769, 13356, 20945, 11390, 155312, 107698, 71138, 12797, 29367, 1300, + 82402, 758, 56650, 14527, 90884, 61424, 194495, 23078, 69669, 175831, 14654, 112729, 44753, 232898, 27621, 99382, + 923, 15115, 22648, 45524, 16939, 11503, 36250, 76362, 59700, 80609, 92077, 81783, 164258, 147964, 93190, 7889, + 25969, 56255, 28476, 115588, 27082, 3104, 94697, 68454, 31399, 203455, 22046, 2756, 43846, 6544, 135118, 128148, + 6306, 148500, 12599, 221886, 246093, 1024, 14109, 13441, 51342, 119104, 168855, 81131, 6153, 19221, 79225, 10911, + 151581, 83444, 1795, 32090, 202801, 86292, 19784, 98045, 182731, 14239, 27382, 126354, 56475, 255797, 116118, 46059, + 162188, 48612, 34197, 23712, 89426, 12944, 100256, 15683, 141356, 108578, 128764, 17528, 14355, 271397, 13200, 5464, + 121815, 164025, 19217, 145007, 27536, 7271, 11898, 27670, 28023, 2020, 34004, 98721, 65257, 221829, 10108, 127625, + 77523, 58581, 6914, 16301, 106668, 18199, 38690, 49947, 127314, 61987, 99158, 82020, 24947, 156159, 44297, 40008, + 12790, 191552, 58504, 13996, 38796, 49380, 31168, 78568, 169698, 51893, 110268, 42109, 23555, 66459, 81335, 350079, + 10725, 11018, 5903, 58576, 44573, 24333, 7047, 15096, 183083, 25940, 30092, 21527, 42088, 116405, 102404, 91943, + 62716, 83434, 46226, 148493, 43265, 16668, 120110, 99611, 105958, 52689, 135626, 19929, 32050, 1904, 97814, 125365, + 44067, 3747, 104, 129365, 50118, 22240, 81479, 147860, 1668, 21592, 58604, 65314, 3874, 8813, 70784, 17613, + 6243, 68475, 42985, 56814, 318411, 38092, 62855, 5047, 16599, 38145, 13374, 7748, 1691, 63047, 41837, 61793, + 94999, 40014, 231631, 149314, 52378, 17178, 17229, 20594, 28671, 298969, 135669, 3725, 216728, 259469, 33874, 54293, + 123258, 45594, 132414, 22290, 2059, 129670, 52137, 8994, 34969, 83516, 130408, 123610, 69225, 72380, 90635, 47420, + 5913, 73509, 87799, 48451, 136280, 23988, 84653, 34987, 171443, 34172, 131573, 84347, 141515, 41151, 84753, 26644, + 91662, 53005, 30287, 228834, 22175, 85089, 5937, 239357, 135282, 3173, 18644, 35622, 80020, 157977, 39079, 53223, + 92270, 5878, 10450, 49133, 1663, 158962, 37020, 13802, 4808, 5512, 34099, 49182, 4482, 39140, 15951, 157071, + 3495, 132172, 122206, 61350, 34691, 43353, 72769, 74295, 5226, 24234, 167378, 239776, 109830, 5797, 48280, 64587, + 108512, 25762, 31651, 55137, 17342, 87230, 251069, 6824, 107488, 60185, 7752, 14553, 11606, 10402, 78777, 6829, + 123190, 43191, 33662, 9649, 100247, 18941, 76354, 27419, 29666, 53504, 129460, 19206, 146527, 208486, 41206, 37237, + 113014, 30191, 57416, 5183, 15794, 3541, 57420, 56560, 30894, 159900, 136009, 282298, 13224, 83119, 1406, 240017, + 39585, 278159, 141007, 6722, 243192, 21680, 25193, 108277, 22351, 16677, 47180, 52089, 9903, 7391, 70117, 38331, + 7836, 20514, 176863, 391251, 47699, 49431, 142718, 40783, 11078, 28591, 32120, 38193, 25468, 458592, 10098, 64745, + 122291, 62128, 93822, 46652, 48821, 13662, 8766, 17814, 26780, 22468, 135823, 124150, 122679, 129978, 11360, 113819, + 75521, 58918, 556, 93437, 81450, 77000, 1965, 3492, 630, 26563, 1323, 193118, 4895, 24288, 80421, 444827, + 92900, 45787, 6232, 74549, 55074, 50259, 138542, 21220, 74293, 14177, 86741, 20845, 17441, 108129, 27172, 542, + 3563, 94640, 76494, 41896, 111657, 28829, 50902, 406663, 103102, 28983, 10586, 120692, 51613, 18805, 55476, 84562, + 12318, 65439, 2351, 47041, 52370, 9553, 44550, 41604, 36191, 65155, 67526, 37264, 68245, 92017, 11541, 5842, + 34269, 49599, 26889, 59557, 40445, 96850, 1906, 135711, 41354, 29246, 70287, 106660, 122901, 45259, 163034, 61670, + 168604, 10682, 20890, 849, 182500, 7459, 14076, 9824, 62012, 17393, 43, 55379, 42557, 140724, 7749, 356813, + 11259, 106585, 56522, 117254, 24428, 38947, 52871, 9300, 115113, 20086, 55518, 10927, 86345, 43851, 62143, 258476, + 12362, 54324, 10491, 40991, 3909, 2374, 125973, 57172, 78430, 341578, 168875, 62670, 86852, 965, 24965, 107818, + 134602, 52017, 33775, 46473, 20459, 104509, 19147, 87014, 47853, 11886, 132839, 96809, 93879, 110609, 2365, 42726, + 22577, 113945, 93171, 450, 7659, 3573, 145278, 14002, 3688, 110115, 18705, 53062, 38555, 47342, 56746, 32670, + 13349, 82125, 43630, 40651, 17381, 125502, 22739, 15199, 56715, 30071, 11269, 266060, 91639, 147670, 110730, 156150, + 12493, 33798, 89625, 209004, 10895, 25311, 4017, 18602, 92438, 67412, 21469, 72030, 3142, 102900, 25974, 86939, + 37057, 197485, 58360, 6360, 28928, 67451, 1402, 28843, 2004, 88633, 12339, 31963, 36427, 70216, 321089, 288373, + 32268, 39008, 31413, 4740, 34222, 25661, 12279, 159470, 209974, 23583, 8221, 132962, 10432, 69680, 62983, 6760, + 67436, 1150, 66541, 28543, 41989, 120559, 51407, 30922, 173518, 85466, 111050, 56221, 107930, 37565, 20849, 48181, + 27079, 107182, 10708, 12667, 62729, 131120, 15921, 10076, 30908, 157502, 94904, 63675, 55558, 6617, 226273, 5180, + 5828, 56366, 13233, 61985, 45031, 2485, 22785, 51794, 14902, 861, 156114, 2399, 53546, 23616, 69393, 128641, + 8204, 20069, 47928, 35756, 144263, 9724, 28168, 77879, 60255, 33275, 30360, 2287, 14520, 139561, 6919, 38561, + 88212, 24401, 123947, 83850, 86582, 113753, 3963, 10915, 109589, 75957, 42047, 64212, 69356, 27884, 27654, 1658, + 8064, 25531, 26396, 43262, 47449, 97110, 27015, 18044, 8505, 10820, 6910, 15078, 66558, 7192, 123575, 29932, + 16886, 91286, 41104, 63367, 4844, 40914, 6359, 2845, 52817, 88113, 9448, 105511, 111260, 18932, 4691, 644, + 215129, 199771, 74460, 6293, 12941, 6495, 60621, 58746, 91118, 57660, 16174, 48568, 650, 73549, 62408, 27497, + 20770, 36570, 9691, 125291, 6273, 101510, 524873, 63355, 73089, 27873, 22599, 43928, 40618, 143174, 44188, 54641, + 62790, 6716, 38742, 153007, 2873, 81054, 70058, 28685, 35002, 11708, 63922, 161592, 14023, 143955, 9908, 36977, + 97208, 20840, 37740, 37328, 19386, 541613, 59406, 52601, 102646, 24863, 90336, 108723, 36993, 19256, 90283, 156507, + 143736, 226099, 22792, 168470, 135457, 88686, 29520, 41685, 35385, 2247, 53884, 7914, 113601, 25634, 168024, 22823, + 17893, 52964, 50078, 12971, 32627, 62833, 89378, 69345, 84439, 2950, 9888, 39211, 100619, 12559, 4264, 76283, + 56016, 57173, 88905, 18361, 6581, 257, 15516, 18690, 57264, 152842, 167732, 100142, 172160, 177909, 114841, 98812, + 45452, 152952, 146652, 57421, 111710, 33059, 83570, 109892, 203627, 18224, 13991, 11405, 70131, 54654, 97200, 72333, + 24100, 8565, 145467, 50366, 322787, 161041, 7765, 55869, 1996, 48165, 55619, 16231, 35665, 59444, 21986, 40608, + 70078, 91914, 2452, 11983, 22358, 46435, 113727, 327395, 90922, 54176, 3637, 61910, 83658, 16892, 19473, 12132, + 4097, 10440, 32159, 34709, 63200, 53820, 279, 973, 58499, 93368, 5855, 59071, 14542, 256523, 219447, 28720, + 99153, 71396, 152707, 32750, 52159, 60623, 48369, 4454, 6615, 260119, 265523, 24086, 12414, 92637, 2799, 18610, + 63415, 61306, 58297, 80979, 31986, 68389, 34835, 18261, 16823, 2311, 3203, 19828, 1579, 46046, 15243, 2581, + 65405, 113461, 50646, 108814, 137809, 9153, 82289, 158349, 8841, 506, 90330, 157611, 16898, 11327, 131769, 58320, + 48082, 107069, 6787, 166721, 259, 104429, 47373, 3486, 33014, 21454, 101722, 52682, 42375, 100879, 112359, 120319, + 12260, 113213, 38656, 173961, 179850, 20340, 86000, 5336, 73667, 112248, 73424, 114676, 91389, 8255, 33598, 167952, + 98882, 313221, 141752, 163735, 29532, 164918, 42180, 9328, 17311, 38319, 9180, 42386, 326251, 90818, 26426, 1863, + 41092, 62380, 5374, 95142, 5928, 29861, 105695, 62497, 20742, 89078, 12203, 22276, 44964, 36172, 50837, 165289, + 48019, 238111, 16046, 2104, 43505, 99836, 25312, 70749, 9317, 81582, 61952, 108704, 49265, 17815, 1489, 44835, + 6643, 115249, 21605, 88265, 192712, 163219, 138454, 48414, 48424, 56847, 23238, 77168, 163487, 20527, 64335, 82158, + 19861, 31349, 187836, 102708, 20113, 52444, 125598, 1727, 70848, 111298, 11424, 155809, 31928, 96999, 24225, 14471, + 105333, 16160, 91323, 49302, 23685, 29832, 218861, 101530, 78563, 93096, 11349, 110852, 14638, 43607, 123209, 91558, + 54755, 35628, 107357, 380618, 7158, 28912, 18284, 4868, 24142, 51577, 38642, 239650, 44018, 991, 26090, 1960, + 20774, 158291, 81907, 50593, 125255, 6485, 72743, 2966, 20331, 214561, 56587, 142157, 24280, 12387, 150, 57700, + 10163, 773, 45875, 97502, 1285, 41252, 206433, 50923, 2336, 113158, 120454, 130298, 39851, 20223, 103681, 48431, + 4299, 190321, 123059, 33343, 117269, 71169, 26222, 33921, 46714, 32383, 97444, 133272, 63816, 37830, 49882, 50473, + 87779, 17666, 112641, 59034, 159624, 26706, 33574, 47126, 11731, 63641, 16669, 933, 9971, 20216, 30684, 99703, + 990, 23911, 20195, 121307, 137317, 47148, 41928, 10318, 108831, 355535, 49454, 32292, 50994, 8122, 16384, 59812, + 74554, 32150, 56591, 174312, 162680, 190228, 9598, 173148, 23640, 61991, 45718, 79874, 131597, 38994, 5964, 48294, + 146962, 7849, 20626, 126616, 170620, 27716, 30327, 39879, 34829, 15772, 147440, 111326, 91205, 762, 39309, 61975, + 21184, 41411, 43771, 23877, 1913, 5640, 44358, 29145, 63616, 7290, 39142, 49951, 18427, 231174, 53813, 32775, + 93136, 7356, 69502, 40528, 156592, 58725, 18403, 57219, 17519, 27403, 39497, 64617, 67565, 18802, 347190, 3273, + 115882, 171128, 6465, 87835, 138220, 37443, 12843, 45934, 78622, 101762, 40426, 16768, 88535, 152513, 46216, 13386, + 18115, 79078, 49051, 26866, 2711, 79682, 103900, 31529, 33554, 8114, 97575, 47612, 109492, 22435, 45316, 90147, + 54298, 70069, 6113, 94219, 971, 203886, 171510, 54098, 24914, 22343, 6068, 91601, 25863, 143464, 64459, 38876, + 36363, 74653, 13924, 17888, 45715, 21595, 79494, 40396, 27099, 48946, 229942, 68108, 194995, 38040, 6364, 105256, + 14299, 43792, 53868, 72865, 178181, 10801, 106043, 59704, 111488, 12611, 11620, 1615, 72395, 35357, 68088, 375, + 322385, 97707, 2721, 26698, 157719, 129043, 96424, 15347, 130787, 1519, 70008, 98397, 11897, 5720, 336769, 151561, + 81843, 3436, 2996, 78953, 83999, 15800, 40841, 8897, 11369, 50915, 1888, 69080, 49280, 45808, 1062, 11670, + 118604, 172349, 10206, 107337, 40922, 127584, 10458, 54750, 61332, 36780, 6193, 84096, 110343, 6105, 74775, 66665, + 53407, 65182, 51401, 281520, 75639, 25185, 147990, 19365, 40582, 32622, 61482, 106248, 300440, 3435, 158683, 10556, + 54722, 4592, 47332, 14499, 25637, 77065, 19723, 29586, 13694, 66649, 26726, 58610, 48248, 133393, 123123, 232634, + 48278, 828, 68414, 56873, 194521, 5023, 64229, 14306, 56203, 86618, 265580, 48023, 52779, 84656, 3432, 27698, + 48783, 165900, 55898, 24382, 72627, 95034, 100547, 13185, 10953, 158080, 190907, 81943, 376, 38482, 157736, 4714, + 16733, 10368, 160286, 14619, 280238, 20156, 17445, 38808, 26351, 35060, 125894, 64807, 230789, 9972, 49181, 669, + 15132, 16923, 2631, 4706, 25168, 70685, 110447, 122549, 137270, 146057, 155535, 21929, 3588, 7274, 38517, 20678, + 63704, 12863, 1324, 50985, 73376, 43993, 200585, 60934, 94031, 33647, 19008, 24301, 74284, 50725, 56271, 104028, + 19443, 69093, 1021, 4350, 159557, 108365, 31846, 2318, 9697, 23630, 70960, 33088, 39901, 32656, 33002, 83862, + 13351, 27646, 142809, 48055, 119050, 37694, 118047, 85271, 15406, 13547, 124197, 30179, 146455, 65071, 112066, 5002, + 3460, 7809, 11639, 39385, 29556, 54914, 121220, 5596, 75195, 25679, 106873, 33104, 37673, 7246, 204139, 29952, + 102524, 115109, 36038, 5460, 92329, 49344, 25242, 34014, 47289, 113697, 6996, 24397, 98413, 185550, 45812, 60522, + 15311, 46467, 213965, 11122, 100684, 7259, 17039, 6550, 56345, 167013, 404753, 307, 7116, 56462, 5417, 73857, + 95480, 78473, 10133, 19036, 11590, 36107, 100941, 53208, 7200, 60420, 95105, 119370, 167, 15524, 6653, 43384, + 23610, 46388, 13093, 145345, 58426, 55680, 305451, 86847, 17730, 58567, 178140, 38965, 136656, 5628, 88811, 17675, + 27944, 83441, 51603, 10939, 53151, 35212, 27904, 53967, 2701, 23131, 24488, 104154, 8824, 55304, 27690, 42802, + 103124, 98578, 280054, 5662, 3017, 22793, 12906, 31495, 90744, 23983, 3464, 134399, 113588, 22474, 69068, 5099, + 53216, 109191, 117048, 132077, 79736, 676, 7774, 71253, 65940, 32772, 116614, 53644, 26931, 7596, 62478, 12003, + 498, 19827, 108263, 47076, 29568, 168754, 31319, 6482, 80540, 13955, 111749, 21181, 143543, 7033, 26026, 51353, + 21292, 85463, 42779, 24887, 1740, 14404, 130369, 63220, 59268, 97587, 33374, 6212, 16561, 43680, 36822, 42418, + 180816, 18436, 9579, 74123, 42323, 147608, 27260, 24977, 50174, 61507, 10681, 205404, 40890, 85856, 53671, 11516, + 52866, 109367, 117903, 74687, 10703, 42095, 18194, 187166, 57169, 32725, 8992, 137746, 4700, 203872, 80833, 23954, + 17191, 61462, 362182, 13934, 4424, 17464, 76670, 12727, 93511, 23063, 65868, 71113, 49698, 1101, 33252, 57054, + 166650, 125436, 48999, 126818, 26972, 5103, 5669, 11598, 48631, 1882, 120131, 84296, 165169, 35330, 3827, 13014, + 82879, 103279, 159135, 24477, 69326, 96729, 88413, 114734, 202970, 9725, 43640, 83623, 4007, 109, 63529, 18092, + 2376, 47112, 60271, 36164, 231325, 18597, 8868, 14797, 139592, 33580, 9437, 22241, 22119, 132398, 126879, 35116, + 62851, 44339, 72086, 46648, 37504, 19320, 81584, 48809, 68816, 14329, 24943, 2579, 58345, 39374, 43873, 7046, + 67398, 497785, 28225, 24722, 186643, 41257, 163961, 110230, 43331, 10072, 100738, 91543, 277416, 48225, 37125, 93902, + 53749, 3511, 89864, 7794, 15746, 112345, 53732, 9563, 23102, 195132, 38187, 21737, 17432, 246976, 175275, 118156, + 4793, 40293, 65384, 52281, 151138, 59278, 5431, 12606, 48822, 47893, 70528, 6708, 54265, 9483, 244562, 3448, + 48203, 48689, 76888, 27624, 198688, 30465, 4820, 115925, 14305, 15512, 85317, 22487, 54287, 116889, 25533, 69671, + 2291, 33041, 27717, 4838, 18018, 20643, 214146, 60357, 113378, 9119, 25030, 79694, 123260, 45067, 41450, 70902, + 7180, 58600, 349673, 18393, 97549, 78338, 21978, 83130, 87027, 776, 46804, 136813, 120085, 10785, 36277, 36292, + 2920, 3810, 8562, 21587, 76080, 26439, 44502, 4907, 8190, 14533, 46271, 56528, 102005, 40453, 71405, 78573, + 5641, 71512, 18178, 33524, 64580, 92871, 1535, 33803, 14955, 66991, 45301, 14937, 59802, 4319, 76815, 79999, + 54028, 152893, 140972, 94339, 58884, 25420, 33237, 94907, 19367, 63221, 14998, 39549, 81779, 200006, 11084, 130155, + 412567, 22144, 57584, 48001, 85957, 43644, 62293, 11070, 97053, 131816, 56871, 43893, 103637, 49559, 149443, 265832, + 78871, 127928, 16848, 80586, 29364, 10123, 96121, 61469, 27637, 122786, 76840, 137205, 141728, 152810, 275825, 164900, + 4767, 48483, 59279, 45329, 30686, 30546, 26125, 67059, 112738, 1079, 36371, 14899, 130146, 140588, 27019, 105371, + 42745, 50423, 37529, 61488, 12730, 34379, 84661, 32169, 105040, 91899, 1842, 106489, 14844, 44986, 133939, 80141, + 232, 29291, 19684, 156429, 210944, 61928, 43419, 24020, 36581, 39425, 29731, 17427, 152317, 27381, 12301, 14375, + 135543, 5697, 113605, 107512, 29744, 107850, 5566, 76470, 3129, 115721, 13033, 87708, 55647, 1993, 5259, 8785, + 58149, 147004, 120371, 11205, 46319, 4743, 14434, 21697, 27265, 15562, 219250, 35228, 17499, 95327, 51051, 18310, + 28005, 166872, 77694, 6553, 59948, 33504, 13613, 41235, 7170, 2462, 19927, 201918, 34138, 6665, 48780, 7020, + 5702, 48429, 19212, 60564, 293047, 10831, 70945, 188451, 110892, 514, 102527, 8278, 408, 60992, 54201, 111510, + 91760, 50530, 1350, 10771, 218674, 53990, 187697, 687, 18469, 10560, 105023, 46885, 46095, 53517, 5808, 50818, + 81403, 2170, 22662, 47127, 14389, 748, 107058, 67959, 4610, 94719, 30947, 36510, 35672, 12468, 50291, 29921, + 73060, 20353, 3271, 51468, 11006, 18654, 11663, 76382, 74848, 7193, 20342, 16187, 104820, 128163, 34980, 13510, + 118143, 40642, 131, 15981, 190357, 1266, 49918, 3612, 20043, 144329, 29109, 51118, 105358, 3728, 9721, 32144, + 141735, 43619, 23028, 307278, 5115, 112747, 69300, 39045, 27093, 43684, 177727, 78108, 45924, 88098, 14492, 21583, + 123073, 18169, 28525, 17923, 52599, 21514, 7009, 272, 29433, 16693, 69214, 94247, 9616, 71738, 25205, 9774, + 238350, 23631, 52998, 161274, 78610, 112631, 168968, 6162, 24851, 53162, 47254, 154989, 58858, 17143, 112681, 166473, + 26769, 71077, 10680, 22270, 31969, 225745, 12087, 104993, 24613, 70335, 68735, 124630, 18294, 157460, 57255, 111655, + 4982, 2471, 133743, 52649, 32735, 12031, 93961, 124470, 39639, 39402, 65329, 63203, 143563, 23805, 22268, 105529, + 112073, 16680, 215716, 21519, 202205, 53228, 247084, 41944, 12567, 2653, 65493, 34693, 4873, 34636, 23926, 71532, + 88601, 71037, 5571, 115122, 44897, 69538, 89072, 43808, 81503, 36583, 28920, 133940, 101648, 191184, 4910, 9942, + 81362, 14489, 27729, 4960, 34662, 1289, 52544, 14776, 85277, 65813, 125227, 8902, 17574, 23259, 36661, 1163, + 48173, 42427, 11467, 98879, 21435, 10466, 35373, 39046, 221188, 185252, 93230, 99061, 40215, 21457, 5235, 36240, + 39576, 128066, 115000, 60589, 80786, 34006, 111892, 51482, 26544, 10656, 50198, 141955, 64668, 129479, 51135, 64929, + 81841, 27845, 7128, 18886, 10731, 11965, 52276, 82304, 37733, 19729, 35028, 59846, 247986, 66135, 53707, 299634, + 149188, 117025, 39304, 27726, 127703, 9496, 71006, 9486, 495, 94820, 47250, 266544, 18382, 101150, 6281, 60703, + 54388, 207915, 132866, 58949, 72446, 26212, 10542, 172127, 43071, 62490, 4376, 56788, 30974, 24968, 91443, 11619, + 198723, 24001, 15121, 136267, 89608, 160663, 33578, 82492, 41360, 46571, 2169, 9481, 190, 29283, 38373, 141023, + 33045, 7619, 29792, 83332, 8386, 98498, 39183, 39255, 31658, 129170, 27144, 67660, 19992, 114184, 53128, 24534, + 237838, 8463, 70363, 22205, 119015, 18826, 68254, 121728, 137622, 10868, 292971, 14603, 50890, 34277, 24430, 65197, + 100913, 109419, 40103, 47081, 6460, 25365, 62207, 31961, 116233, 10816, 65775, 61031, 267230, 19629, 72115, 104486, + 26621, 27002, 49635, 13334, 104129, 8378, 107732, 16558, 65114, 17477, 179978, 4220, 14190, 224493, 16453, 41872, + 41542, 19134, 32179, 25813, 14888, 130609, 58701, 15362, 85962, 12751, 55045, 6635, 23342, 28105, 4224, 127171, + 23041, 34303, 59129, 73656, 26453, 19410, 24291, 79837, 43725, 157265, 16625, 34377, 71809, 89485, 82437, 24305, + 45186, 20785, 34798, 76873, 4770, 3923, 44346, 9874, 46452, 55654, 54665, 4785, 53894, 5025, 60017, 171215, + 56616, 15749, 17762, 16761, 221286, 105132, 20523, 56343, 18973, 1037, 602, 323886, 9038, 95606, 38396, 120502, + 109299, 77097, 79367, 136893, 55365, 8323, 10041, 75592, 19366, 65814, 15614, 83469, 26863, 51521, 18119, 15623, + 18808, 146873, 172424, 11543, 60909, 19528, 22504, 437698, 69353, 57600, 115924, 4644, 41738, 42360, 79256, 27642, + 83463, 162712, 19094, 2916, 12100, 118144, 293484, 54555, 68561, 61908, 73882, 178688, 72860, 49139, 70532, 39905, + 3980, 13190, 59763, 20658, 13796, 126559, 22696, 105248, 49340, 64706, 49195, 81589, 12332, 67817, 7722, 132448, + 31311, 20180, 151712, 35160, 27418, 221689, 96589, 5129, 4255, 9628, 16047, 98064, 53430, 33666, 22032, 208500, + 18976, 61564, 112915, 15902, 45523, 84205, 34747, 46386, 510, 76620, 34577, 24704, 14224, 18606, 4384, 109151, + 30477, 37359, 74952, 30874, 26581, 19550, 21657, 13283, 4530, 93073, 46376, 122364, 3651, 181757, 62601, 2206, + 101663, 118572, 17855, 52952, 139840, 62606, 278021, 77080, 22709, 7959, 241332, 42371, 150861, 39772, 27577, 14415, + 31996, 13012, 109120, 42856, 63923, 41223, 14878, 182608, 120623, 29348, 36991, 15117, 262522, 7183, 54934, 3332, + 3076, 65389, 287192, 49740, 10528, 35270, 56818, 22639, 2929, 61578, 23280, 30127, 14672, 3157, 47696, 21588, + 130238, 62110, 52327, 56193, 18087, 130896, 142685, 109840, 9816, 18121, 57992, 97319, 121894, 80599, 54501, 27268, + 100308, 81451, 3014, 47285, 25085, 7174, 5312, 74894, 55111, 160804, 29625, 46682, 14565, 172807, 29181, 36368, + 18952, 75109, 124625, 24016, 53293, 186013, 58761, 87814, 2042, 89459, 35087, 63052, 369988, 86526, 54374, 118097, + 23674, 2122, 110119, 15845, 61789, 10677, 100968, 183270, 133529, 81233, 103267, 198839, 28783, 73169, 17965, 361713, + 108293, 136324, 42487, 26373, 35477, 218558, 27158, 71527, 47119, 331883, 45160, 78970, 36448, 156591, 71832, 96730, + 71049, 146, 111991, 17781, 40015, 99088, 5627, 90890, 33055, 23338, 4252, 167723, 78598, 149826, 35613, 11228, + 198442, 31532, 169906, 23323, 1833, 125495, 8102, 12897, 159937, 4522, 13990, 177607, 40654, 227310, 20082, 31598, + 77444, 15467, 109179, 17723, 189245, 6218, 103555, 3422, 113153, 7920, 38581, 148584, 8621, 19962, 49090, 28195, + 18599, 79279, 133222, 12709, 38553, 16730, 103630, 60913, 35223, 13228, 67419, 28730, 166072, 36003, 6493, 36652, + 2375, 50657, 9527, 81998, 11659, 334160, 15168, 51380, 21786, 15122, 59275, 119745, 89523, 67219, 2185, 46735, + 6032, 47447, 137690, 5166, 12116, 23018, 5022, 48313, 63046, 10829, 83429, 2746, 159398, 67335, 98418, 53999, + 18454, 31569, 37892, 109132, 3678, 81620, 104491, 54681, 32521, 109973, 42533, 52504, 47626, 42504, 36807, 2633, + 11411, 159915, 60573, 111310, 103527, 128644, 103027, 285252, 38896, 10950, 44585, 164005, 42946, 28131, 45971, 32359, + 15696, 11312, 106951, 2917, 26370, 12048, 16052, 74680, 10185, 98893, 89672, 24790, 8413, 87026, 851, 4549, + 37080, 174727, 18289, 200644, 165583, 57864, 69857, 29259, 4331, 19717, 91462, 13011, 63555, 21391, 1990, 44481, + 14907, 7340, 15774, 49591, 72220, 3931, 6418, 53568, 50056, 9288, 4106, 48438, 6623, 5117, 1012, 41437, + 62236, 84002, 33918, 65542, 36565, 92277, 39629, 127582, 49783, 44146, 117628, 5155, 10049, 42692, 4608, 187456, + 17503, 74015, 34318, 12962, 100581, 33867, 21832, 157914, 55951, 106858, 14475, 188259, 146244, 57414, 21163, 26020, + 24724, 5253, 161736, 121717, 9626, 38446, 431177, 1188, 17969, 46978, 157547, 62747, 25524, 30400, 177415, 12289, + 109300, 175621, 49009, 166322, 173965, 132234, 51768, 26032, 99994, 15755, 29800, 72351, 101056, 240613, 21289, 15214, + 46459, 46348, 58410, 30396, 43647, 89731, 28915, 142785, 53737, 14946, 5545, 24313, 277968, 72809, 33232, 8640, + 8347, 16712, 3946, 25575, 123521, 39179, 9991, 145543, 74858, 137200, 34491, 8629, 33829, 23820, 68921, 35423, + 44762, 49871, 133748, 46991, 77574, 166611, 31020, 11875, 877, 44133, 33877, 3257, 81377, 73657, 30727, 46882, + 222525, 70076, 137697, 69291, 123532, 18001, 25423, 188796, 30602, 16643, 53078, 60931, 43881, 38283, 3221, 355554, + 53145, 80702, 59009, 39689, 2973, 60883, 77304, 1117, 16284, 24073, 22669, 122990, 81940, 39844, 12369, 18781, + 61281, 133978, 1107, 20779, 127044, 44416, 157395, 5868, 63620, 24846, 35621, 48921, 9875, 41926, 56949, 22208, + 14756, 163188, 3847, 68103, 114829, 35532, 76171, 42852, 19032, 34658, 126394, 15837, 9202, 15195, 40076, 88927, + 52759, 150700, 23019, 8077, 119141, 224558, 88676, 134306, 23928, 48525, 115416, 76405, 120551, 21229, 51681, 188804, + 19607, 12750, 33280, 20117, 3599, 57084, 87953, 20205, 33401, 81760, 13064, 16835, 76821, 64466, 84841, 46288, + 73233, 87118, 9264, 13911, 117430, 86790, 13771, 24443, 39968, 82124, 15778, 63864, 36539, 3066, 24571, 55240, + 7071, 149040, 28850, 48106, 5446, 69537, 2674, 42691, 121735, 29802, 93801, 222967, 194059, 30066, 53170, 4677, + 15206, 107943, 39854, 31639, 45283, 53779, 59063, 2348, 6706, 195398, 48495, 2005, 15603, 165902, 265960, 30910, + 65615, 207693, 136663, 32249, 1207, 87403, 19215, 33091, 165723, 49438, 43462, 11067, 92275, 62699, 44678, 45289, + 34773, 17412, 48972, 1858, 104447, 181545, 1001, 119266, 8396, 153464, 132289, 115529, 32353, 7992, 172028, 54069, + 205240, 29592, 91739, 119994, 164323, 113460, 108440, 30929, 13600, 17141, 120659, 18146, 60555, 88204, 146500, 273964, + 79205, 25175, 90166, 44725, 25532, 113251, 4468, 67884, 22907, 53542, 14201, 46217, 33410, 54978, 26399, 177809, + 57480, 121936, 45917, 11309, 107111, 19480, 6478, 34292, 69630, 3736, 25310, 244475, 32137, 29438, 9982, 103486, + 47832, 52490, 37250, 2562, 70913, 5179, 136415, 91803, 33161, 9255, 18432, 13038, 20321, 49485, 6575, 115334, + 2371, 33789, 7808, 45775, 117348, 14605, 42941, 98279, 10714, 95052, 76922, 21407, 86246, 16306, 223273, 15316, + 1625, 250738, 2661, 32095, 11763, 40925, 49073, 29377, 17900, 1538, 17882, 8, 268, 90833, 50429, 806, + 78457, 6040, 177458, 105879, 99175, 24946, 48684, 55552, 97940, 1832, 81954, 48107, 101092, 1204, 34337, 93721, + 86660, 78345, 46377, 86370, 32221, 80601, 7480, 46341, 14041, 65429, 7269, 59499, 128504, 5442, 8744, 14012, + 125080, 161674, 6784, 14584, 53744, 130216, 6841, 122602, 124263, 31593, 18807, 113744, 31017, 885, 20344, 220948, + 13897, 791, 83914, 15043, 403, 15272, 20903, 38772, 31859, 100639, 117558, 55130, 21964, 75452, 165550, 31144, + 5633, 18734, 4133, 25708, 111630, 157700, 635, 222034, 5547, 90494, 37283, 12982, 77329, 31122, 83775, 43379, + 17961, 19477, 64730, 37597, 18241, 81216, 57923, 24610, 84995, 11492, 22811, 22078, 25984, 270901, 50439, 91195, + 12983, 70850, 98931, 154004, 67491, 66969, 83799, 168671, 62168, 141041, 109484, 189545, 47262, 124643, 76032, 11984, + 5241, 73085, 225, 35278, 297, 65251, 11750, 150754, 51191, 49847, 161229, 99388, 7351, 7406, 114189, 8814, + 8967, 13541, 57080, 44100, 147212, 116349, 149864, 40247, 82060, 41029, 37322, 87508, 16821, 59609, 20679, 68392, + 782, 43253, 332, 39938, 11033, 23732, 188925, 38934, 82431, 93785, 15532, 191109, 62957, 63022, 27531, 12252, + 5026, 3833, 2431, 21465, 43459, 25953, 192718, 3308, 77963, 89356, 168314, 168685, 203477, 49146, 7655, 23219, + 53528, 24982, 48685, 54631, 6247, 793, 74260, 48185, 191852, 3095, 172876, 179270, 87774, 154528, 7635, 7686, + 74164, 17007, 112569, 121364, 215654, 111268, 13329, 18967, 13467, 122533, 140331, 36832, 1522, 10077, 18418, 42747, + 219964, 6033, 9858, 108306, 28589, 3913, 123863, 178765, 244104, 135651, 14684, 90, 16242, 77118, 55574, 8651, + 117821, 131103, 73529, 284295, 67725, 19246, 36974, 62269, 72570, 115836, 24484, 7412, 156792, 33054, 573360, 105103, + 17186, 54299, 4686, 38563, 15979, 5729, 49563, 346297, 26990, 61110, 74313, 185151, 44128, 30657, 15134, 17195, + 193014, 25751, 282233, 137910, 35276, 2087, 2963, 41221, 57125, 17228, 387574, 26559, 16212, 16812, 134202, 221232, + 166451, 26462, 936, 117519, 68017, 293962, 22383, 16552, 6905, 277202, 9279, 27507, 77608, 63540, 29132, 62909, + 16364, 5654, 9548, 22674, 53777, 85931, 21483, 80245, 75921, 20622, 81952, 19224, 76426, 285538, 17463, 125910, + 37975, 57734, 50774, 69447, 26203, 50278, 2138, 85944, 269296, 11227, 222528, 187351, 64099, 65932, 14212, 61685, + 84122, 91003, 37581, 16099, 12077, 59461, 40868, 28123, 38533, 44075, 2588, 36016, 830, 15331, 10265, 75145, + 4407, 11384, 111778, 70114, 20139, 984, 26926, 342, 963, 46653, 91781, 6704, 43028, 132517, 82755, 31140, + 38902, 59578, 21048, 53790, 42911, 10083, 25044, 133733, 37503, 46985, 29071, 44261, 83343, 70437, 43357, 137819, + 85045, 60306, 103907, 440, 16979, 162235, 12666, 4636, 1165, 37911, 28310, 153557, 60835, 38997, 64064, 5185, + 137387, 32320, 2362, 99853, 58380, 69321, 213523, 50218, 86990, 100948, 61020, 50950, 110066, 13348, 19342, 25918, + 134540, 64060, 4579, 40959, 56331, 1462, 8265, 82320, 193845, 39437, 45216, 6839, 81238, 14382, 79046, 111178, + 17922, 29276, 57365, 149704, 163093, 60574, 16827, 8286, 38744, 18611, 99128, 34812, 110641, 21069, 171408, 84764, + 12502, 14627, 116407, 2999, 56404, 1193, 183004, 176249, 34862, 85940, 4493, 112158, 26865, 58572, 118875, 48672, + 125964, 15384, 25939, 117101, 12965, 233142, 40136, 48959, 111648, 66007, 37557, 264020, 25547, 33774, 91392, 32277, + 7771, 19724, 17197, 71573, 27196, 92454, 77107, 15576, 136980, 15649, 12242, 7975, 9555, 9118, 86424, 147261, + 29551, 122983, 9583, 13549, 107158, 97365, 31540, 14053, 57885, 2089, 19688, 78428, 18831, 57090, 7772, 6023, + 37705, 42279, 17489, 45843, 35505, 9504, 28240, 38671, 101742, 14357, 11556, 49552, 13970, 125707, 62503, 78, + 102109, 90600, 803, 39636, 62548, 22745, 124819, 19312, 124657, 21863, 74065, 15736, 23328, 7341, 72910, 29289, + 11124, 153626, 25328, 14621, 89592, 68952, 122844, 51529, 146376, 37056, 32205, 119308, 248050, 105130, 5385, 9375, + 6241, 68714, 119962, 30149, 22033, 32348, 9826, 10506, 18337, 1194, 11043, 20440, 80685, 7291, 6655, 39825, + 29898, 34836, 89830, 2183, 11908, 77399, 230, 65020, 216623, 34971, 221825, 14543, 67721, 227257, 14033, 27652, + 106162, 21776, 20020, 28180, 146610, 80549, 55471, 219506, 21377, 61817, 7021, 305922, 15085, 106336, 54962, 10257, + 91552, 89739, 15956, 10310, 42041, 12349, 21713, 99929, 62560, 8880, 23249, 59633, 122532, 7065, 9930, 6740, + 125336, 55124, 109173, 72328, 102365, 8889, 8209, 56963, 121537, 88838, 40700, 83507, 142559, 9284, 11988, 12130, + 29693, 99684, 1851, 53782, 223919, 30273, 31841, 19809, 11515, 73342, 35465, 93163, 110495, 23975, 49885, 92376, + 18776, 32648, 125596, 132210, 22494, 29891, 297641, 6024, 5895, 22238, 100417, 18472, 185059, 48669, 125656, 10048, + 103592, 49510, 59226, 104204, 229351, 165617, 46718, 48516, 51220, 41125, 102032, 77586, 100102, 19702, 25341, 13898, + 37027, 2385, 82750, 60716, 257855, 90865, 4079, 14003, 29359, 297913, 47142, 98916, 54123, 14508, 12634, 40487, + 36066, 129289, 2029, 245241, 106493, 5810, 109583, 17298, 12244, 60441, 17678, 23488, 79258, 151032, 54077, 112594, + 32002, 5397, 25330, 17562, 432, 34826, 2236, 30268, 56205, 22097, 10644, 6211, 94836, 184579, 8566, 12259, + 90182, 61802, 2022, 29431, 6726, 4718, 91806, 456527, 14762, 39564, 99255, 8125, 29391, 128901, 131938, 180961, + 48938, 149835, 7017, 26179, 26864, 38950, 491, 20535, 38083, 43407, 78246, 137337, 60364, 35466, 21672, 57096, + 3310, 39531, 1033, 46613, 60192, 3069, 21262, 288395, 14766, 77635, 29064, 99405, 205567, 154227, 179766, 14046, + 57504, 99249, 29131, 64556, 110760, 88476, 228776, 3720, 22649, 302613, 85204, 95561, 24666, 38247, 51104, 2156, + 46333, 117076, 191749, 48805, 21517, 16529, 57284, 34877, 3430, 71129, 30911, 56215, 13135, 122327, 141377, 4502, + 28873, 12208, 183048, 148459, 27052, 44260, 166866, 49683, 158809, 197069, 27559, 78004, 11597, 48052, 1373, 159150, + 20529, 206142, 12875, 61289, 6695, 68881, 22031, 2730, 23138, 107005, 59860, 1509, 22960, 36899, 42173, 70639, + 37137, 51908, 72392, 61903, 45574, 102776, 400, 53585, 6545, 7660, 71705, 48771, 305877, 115853, 19814, 37969, + 43423, 3477, 6989, 9730, 26153, 133461, 19482, 123699, 24769, 73107, 44361, 86286, 59844, 213778, 53573, 18022, + 14501, 100344, 128105, 108914, 10430, 136734, 154864, 41259, 134352, 60618, 70173, 3694, 56169, 29875, 7038, 16127, + 13213, 3067, 10956, 12306, 103432, 86864, 35882, 158228, 49523, 48003, 10223, 41242, 35181, 11563, 115948, 71725, + 13435, 59826, 26330, 104459, 12408, 31199, 198177, 173879, 129475, 28833, 287276, 88609, 64620, 90587, 23797, 20565, + 230854, 40940, 38005, 64403, 77390, 30876, 41640, 111472, 51990, 21480, 5502, 71562, 15653, 50942, 53320, 213339, + 83248, 57060, 47482, 99892, 33466, 94511, 35134, 39153, 44571, 79902, 26028, 77887, 117828, 65872, 48866, 22019, + 51481, 20695, 6816, 15468, 2187, 56218, 56453, 72760, 10559, 23157, 89968, 337727, 68019, 20454, 32792, 91871, + 18021, 19811, 54181, 19785, 54895, 74209, 9868, 171144, 48247, 7928, 165277, 1104, 18354, 39238, 60324, 127799, + 33737, 46441, 21125, 16451, 4554, 138540, 104181, 92183, 108595, 35070, 341328, 119439, 37288, 53479, 244682, 62059, + 39767, 90048, 55516, 10156, 116707, 177198, 93755, 76515, 9175, 160572, 523, 82789, 3726, 1887, 5577, 952, + 108877, 14261, 74693, 168738, 21616, 69086, 93069, 2668, 83684, 168326, 89980, 40475, 49862, 993, 36286, 134733, + 1938, 1799, 17016, 106908, 8543, 213196, 68388, 64036, 276466, 31847, 29247, 15775, 20134, 20323, 38300, 6990, + 108498, 60387, 10008, 41213, 48770, 96703, 53469, 375632, 102254, 16286, 56810, 71072, 31914, 93491, 252525, 99457, + 131520, 360748, 153227, 148240, 185291, 21359, 5538, 120533, 100559, 7782, 22500, 110558, 51890, 8141, 109666, 34083, + 209, 2223, 43177, 12541, 19526, 38778, 39701, 58422, 76471, 57840, 69298, 168436, 50544, 120544, 5588, 4904, + 71814, 6780, 99075, 11363, 99351, 96073, 4464, 71241, 8172, 116428, 160177, 72875, 198526, 85587, 21266, 28059, + 28816, 58105, 282844, 53464, 20419, 81082, 260276, 6827, 9109, 38541, 171515, 229032, 98389, 150153, 15498, 10140, + 136777, 110487, 92940, 42991, 76479, 19523, 10372, 21173, 75596, 28853, 81504, 63448, 30635, 135311, 13245, 254897, + 165417, 61091, 49656, 45428, 48216, 44529, 18414, 915, 120220, 22939, 12949, 64687, 25955, 67099, 25048, 22602, + 211071, 8500, 87865, 14625, 39314, 101395, 81563, 141087, 24308, 43453, 50836, 133590, 32164, 3576, 59438, 10284, + 2559, 731, 9526, 222252, 146280, 87697, 10901, 52647, 43403, 137838, 74591, 33197, 9233, 63356, 132528, 799, + 17947, 41186, 9082, 7417, 90585, 39849, 50863, 90290, 1786, 8152, 144979, 35008, 86920, 156078, 59343, 104076, + 125662, 127008, 111645, 36851, 2457, 87365, 8649, 14841, 64741, 69960, 475364, 42995, 32152, 78057, 117362, 50703, + 32918, 3669, 219900, 142020, 122882, 25093, 37632, 59532, 78538, 31923, 14695, 27152, 44001, 30500, 34140, 87080, + 31723, 21082, 71987, 36698, 56426, 225949, 5796, 1809, 23375, 125781, 9456, 168878, 103172, 15329, 86662, 97895, + 88177, 17270, 67948, 56739, 145697, 32730, 25167, 10080, 52506, 49488, 112733, 42661, 49319, 110135, 27599, 51073, + 68016, 69079, 90323, 101226, 31664, 20967, 163445, 48192, 41488, 41500, 17028, 27408, 18486, 193593, 92824, 60145, + 110400, 6284, 10148, 99305, 7030, 8019, 15366, 78496, 28241, 25688, 95511, 86416, 986, 31151, 29745, 215965, + 109199, 10425, 23864, 62113, 19900, 54264, 8171, 31738, 42147, 21664, 39774, 126916, 56864, 9927, 2688, 85157, + 65287, 33019, 23537, 6543, 49183, 29757, 1968, 29750, 7858, 70193, 1211, 62048, 24000, 35355, 53720, 246, + 30453, 1963, 267877, 27516, 840, 101181, 36344, 426, 16673, 136627, 20509, 19180, 25907, 70101, 11239, 56975, + 68916, 9194, 87686, 38354, 89927, 16469, 7125, 48706, 6309, 37837, 22274, 71780, 158335, 39840, 82643, 75960, + 36407, 49670, 1989, 14982, 199737, 14639, 22843, 120678, 130464, 46783, 21419, 13639, 13137, 127847, 144799, 159573, + 59603, 17603, 2824, 59934, 201778, 13312, 36351, 40000, 195292, 19891, 2315, 33443, 21015, 43785, 3891, 2636, + 42466, 7039, 20196, 103143, 179062, 154987, 42658, 47584, 172561, 40055, 48647, 12739, 89492, 8870, 14895, 170930, + 11075, 73935, 31376, 34999, 180407, 46254, 20836, 59075, 31868, 41037, 39680, 25099, 72493, 14732, 26639, 55562, + 20998, 611, 41758, 4739, 60217, 85527, 54988, 99046, 9865, 99927, 2352, 15721, 19530, 86459, 1125, 66190, + 39274, 9720, 82870, 3364, 130266, 15887, 122881, 84869, 54539, 14106, 293942, 63127, 21623, 10194, 31975, 38172, + 12535, 149, 62630, 75752, 13505, 13805, 8770, 88088, 40641, 208378, 14393, 38913, 73375, 44350, 32840, 22198, + 4087, 10333, 34430, 44812, 85633, 122365, 38608, 134765, 2153, 192171, 2383, 3536, 3117, 54286, 96428, 28420, + 70680, 12317, 147982, 4363, 55788, 39340, 6445, 40899, 92096, 24356, 221898, 26647, 47509, 10792, 131852, 24256, + 98493, 19086, 25386, 75057, 37490, 10028, 45730, 184141, 271936, 286712, 1729, 81050, 151475, 2718, 8857, 7068, + 3032, 70545, 10863, 172184, 16171, 61405, 54726, 3906, 96642, 17628, 2754, 24338, 34106, 1369, 8054, 98154, + 78425, 52080, 132116, 61228, 125761, 72327, 27960, 23032, 19591, 19659, 80147, 103860, 3366, 33018, 35292, 39070, + 19316, 88042, 12272, 53033, 54508, 18901, 21506, 21455, 24183, 122461, 20317, 27159, 50786, 118677, 19298, 4344, + 194248, 97414, 153639, 16051, 91528, 37589, 38898, 5339, 33253, 113074, 39403, 51508, 34622, 24505, 11212, 76907, + 108355, 126229, 77678, 8205, 41741, 10599, 69948, 101917, 705, 49260, 68715, 39750, 3814, 111125, 108544, 115867, + 3883, 144663, 33293, 7255, 108929, 13737, 90748, 14774, 13203, 10588, 17244, 84607, 67831, 29180, 50860, 106727, + 10142, 30125, 120708, 58131, 59754, 66460, 103711, 43126, 68208, 9476, 5110, 156651, 29128, 145052, 14949, 151686, + 84820, 79229, 57233, 1002, 56880, 47904, 10010, 11807, 38794, 32039, 2140, 3550, 24972, 116011, 47660, 76086, + 48571, 130683, 18131, 4450, 40821, 39353, 38655, 33743, 40476, 135664, 142086, 29489, 18137, 67555, 45205, 115281, + 164254, 261470, 1105, 128217, 24064, 28118, 111832, 44643, 236309, 17929, 3024, 138171, 79181, 14368, 22266, 37872, + 11282, 10626, 56113, 43632, 395, 41031, 1026, 152342, 39169, 124364, 66473, 4684, 2013, 12270, 120501, 142296, + 51587, 29013, 3177, 129452, 28551, 23498, 8358, 22595, 9645, 92008, 40742, 106126, 701, 194074, 1806, 54493, + 109513, 2084, 33125, 5622, 115899, 14353, 11000, 60472, 113566, 150803, 15891, 40791, 12762, 53130, 36257, 13420, + 62045, 128143, 272516, 213527, 58322, 32400, 115427, 101476, 103726, 20176, 21502, 355183, 41343, 23892, 15753, 2312, + 40866, 54294, 101785, 55146, 244102, 32705, 27994, 13284, 143816, 157993, 81010, 16933, 2490, 6072, 23250, 13602, + 70346, 112144, 297560, 55477, 40973, 56881, 19001, 130715, 52618, 12974, 11195, 11003, 15412, 7646, 11175, 7316, + 30720, 44809, 123585, 100767, 104315, 95294, 104855, 7533, 38917, 11608, 3316, 171486, 42027, 49423, 58544, 56254, + 93676, 12514, 18792, 45592, 17513, 1245, 48068, 120907, 107418, 26207, 55327, 89145, 20706, 175898, 72652, 100555, + 123890, 3322, 87742, 9460, 13399, 59577, 26311, 16724, 97727, 64005, 14658, 4457, 24044, 49860, 32409, 21567, + 87962, 27289, 14624, 50763, 65606, 23475, 207482, 25541, 44250, 48058, 12271, 88998, 98044, 55548, 2331, 52575, + 65276, 9350, 77725, 56779, 74790, 14114, 85990, 42433, 101473, 7844, 52309, 157284, 19350, 39922, 12466, 21036, + 91570, 95031, 74187, 95245, 1326, 34475, 71765, 28558, 87790, 354, 27116, 254543, 172042, 18412, 35291, 39776, + 7577, 28592, 29654, 42244, 100813, 7588, 48319, 48295, 86896, 40289, 229422, 2570, 85891, 10264, 76701, 52231, + 41512, 127376, 230534, 93605, 108130, 23424, 36739, 233035, 27794, 95965, 35884, 151494, 14875, 843, 49833, 38010, + 71431, 219272, 82428, 214192, 12835, 127240, 14345, 138207, 156250, 50941, 99644, 99417, 58135, 7353, 39821, 29812, + 3759, 7415, 14572, 53830, 22476, 49857, 78175, 14915, 42176, 77853, 6530, 12126, 115873, 21641, 122781, 72383, + 34686, 27915, 136002, 76130, 56523, 25687, 6527, 134727, 73643, 74722, 67478, 8251, 108505, 23843, 145891, 48731, + 51491, 16182, 53915, 16603, 20838, 7395, 115375, 138355, 12721, 58670, 15892, 3735, 32863, 124677, 12604, 11997, + 45700, 14999, 258154, 2720, 29496, 271082, 85521, 39973, 13700, 113046, 44485, 36482, 34294, 47751, 134797, 155904, + 55360, 134690, 21770, 91135, 29206, 144711, 48744, 44674, 155942, 59875, 17918, 64497, 123812, 9144, 234966, 164534, + 7706, 5270, 25090, 20339, 163234, 3097, 133900, 125161, 203, 10715, 9667, 87129, 132720, 92850, 3501, 14636, + 49358, 17266, 21111, 30612, 144431, 122860, 101497, 17673, 8130, 11884, 91167, 88623, 175788, 32729, 35605, 166925, + 35818, 36536, 38809, 2716, 3270, 93973, 82603, 23366, 76832, 61965, 62245, 13893, 25710, 144091, 98814, 44208, + 54095, 236277, 53367, 34834, 97274, 2172, 16858, 49284, 28779, 113183, 4643, 106217, 94621, 164943, 16845, 19253, + 74396, 22592, 87503, 34996, 19092, 146507, 19116, 134652, 128242, 22736, 1007, 132190, 58067, 27936, 48566, 40563, + 20885, 33771, 80664, 989, 14670, 14315, 21661, 187703, 93255, 27617, 245729, 44376, 15107, 49824, 93604, 106721, + 63291, 5606, 153280, 101864, 23654, 28688, 6737, 43584, 126900, 7137, 67499, 145087, 129421, 24707, 105699, 311580, + 59294, 11582, 211232, 92185, 262659, 717, 4752, 31126, 9798, 18631, 28374, 3367, 3251, 154411, 52363, 51023, + 67344, 70678, 261560, 78059, 28600, 18070, 79850, 53359, 44629, 30869, 19073, 64045, 50672, 63508, 37203, 78992, + 29072, 93421, 104033, 26081, 26999, 121749, 113974, 301732, 31526, 3016, 52083, 135740, 23183, 10650, 107815, 49863, + 49175, 1554, 10166, 34286, 165843, 102866, 56807, 29193, 175455, 36495, 50639, 18134, 17282, 14831, 3286, 19214, + 175411, 85620, 44203, 2339, 32022, 31760, 24711, 84552, 45989, 38675, 25767, 121791, 30298, 7929, 8128, 324, + 90690, 46242, 120990, 8574, 78118, 72361, 11333, 68279, 83156, 26766, 288, 27097, 23749, 6805, 96767, 122167, + 35636, 198501, 41641, 29661, 31317, 217715, 8631, 12460, 7069, 78590, 46516, 87449, 80381, 45698, 49298, 44290, + 94561, 24990, 13323, 11057, 133756, 4423, 12607, 21852, 14960, 88023, 10455, 58146, 97404, 38753, 9405, 216304, + 6138, 24563, 206624, 146948, 41065, 115571, 46443, 96844, 78041, 42616, 20236, 11182, 32843, 47724, 12361, 122382, + 16601, 14468, 3252, 1927, 34123, 58038, 88840, 63230, 9559, 119391, 241176, 12638, 146529, 181367, 152506, 46831, + 123377, 113569, 91896, 1930, 96395, 154527, 81091, 102845, 54441, 92585, 4800, 95396, 42012, 27534, 62635, 8447, + 84257, 129409, 12110, 25104, 123541, 51131, 36346, 44078, 10745, 2994, 120391, 4597, 22139, 55241, 51317, 125652, + 106459, 267836, 35138, 15293, 11720, 7525, 66335, 62591, 150883, 14682, 152240, 38920, 172651, 34616, 1931, 48367, + 154996, 53262, 13826, 3119, 110538, 5436, 54461, 84681, 4728, 16350, 250445, 69950, 53447, 94853, 13819, 81759, + 25704, 73967, 266094, 36612, 2009, 10054, 152119, 181823, 71152, 47335, 164288, 150538, 119354, 57186, 395013, 2941, + 21166, 126317, 18555, 208602, 66604, 161278, 109157, 190726, 1429, 126459, 64236, 8007, 216162, 49573, 170973, 56109, + 8637, 53811, 49583, 16200, 122250, 46869, 24350, 5723, 63520, 11915, 9305, 41525, 27180, 215488, 96890, 13022, + 29172, 40217, 11913, 5985, 36124, 147278, 26010, 452445, 276428, 85611, 7347, 12127, 107787, 9651, 6341, 2892, + 77184, 25332, 29107, 20014, 4680, 81466, 12892, 58258, 14952, 10238, 70027, 35013, 104903, 128969, 46523, 15173, + 24418, 103787, 139284, 30348, 14793, 9646, 156700, 29992, 51561, 15377, 15544, 84114, 52931, 20387, 129004, 6592, + 8371, 57164, 36658, 17268, 26342, 292, 19324, 116062, 48526, 23357, 38167, 14524, 7118, 263554, 155894, 242917, + 92066, 33509, 27485, 107820, 67280, 197352, 32547, 18520, 40653, 68664, 90091, 69796, 8847, 81434, 4751, 106853, + 34597, 37923, 18335, 221713, 105438, 25972, 14464, 111785, 14198, 134281, 5575, 71227, 50163, 102398, 17570, 101686, + 61188, 22480, 52263, 56951, 146286, 15008, 39203, 25408, 50315, 2155, 73767, 13685, 41205, 94069, 6395, 180692, + 170829, 2835, 2103, 11342, 161496, 89214, 6026, 1302, 585, 38860, 110361, 40025, 197359, 39048, 82914, 220968, + 95056, 26946, 162638, 65364, 1687, 34647, 33433, 75300, 365794, 57865, 121192, 320880, 91349, 18120, 6915, 106455, + 48507, 20917, 41330, 101793, 5804, 46924, 91838, 39919, 49263, 77778, 119562, 26736, 5146, 16996, 48515, 149815, + 104902, 87328, 6533, 151190, 96365, 32904, 4018, 7595, 117343, 138520, 33658, 181860, 132222, 36765, 26173, 59136, + 46084, 53703, 43164, 216719, 96919, 95045, 33006, 96990, 16875, 83496, 45441, 27720, 8073, 17015, 124868, 179271, + 262381, 118057, 199816, 4275, 79982, 123100, 54391, 90904, 52663, 27953, 77709, 4272, 13928, 39051, 71112, 413271, + 16056, 87986, 33056, 18596, 153908, 120302, 27857, 128911, 15145, 60790, 4586, 67318, 109256, 79780, 45306, 11932, + 132308, 106229, 37412, 1507, 18763, 133449, 58137, 37961, 24904, 86499, 21760, 41606, 167644, 77227, 120713, 43226, + 13618, 151594, 108301, 101213, 40750, 66225, 16687, 80402, 18686, 48613, 45656, 10805, 147124, 34576, 19977, 157309, + 114709, 36792, 223317, 58062, 150038, 9205, 150642, 21252, 52849, 184323, 41421, 43314, 2938, 99855, 60463, 129217, + 12568, 75505, 125705, 141476, 48617, 43014, 23373, 19138, 8778, 94674, 178893, 8058, 5459, 94724, 266341, 80369, + 44202, 274013, 86858, 78320, 44591, 24273, 35983, 13078, 74914, 24190, 202665, 27165, 17183, 37327, 73294, 34055, + 248689, 18437, 74717, 975, 13878, 8774, 64644, 71823, 7822, 6524, 56622, 7221, 80060, 16273, 88677, 19383, + 23116, 127134, 154899, 68336, 194037, 241, 4615, 387990, 18487, 147941, 15391, 26006, 2067, 26484, 4709, 66156, + 7798, 2820, 26469, 48765, 43077, 66027, 3606, 27342, 33678, 18421, 7829, 55334, 244028, 32856, 8103, 147672, + 31320, 49696, 49702, 69018, 74273, 120264, 122020, 31808, 2794, 1867, 52845, 55295, 19466, 19329, 26414, 52167, + 8218, 210642, 13654, 127707, 36280, 64707, 8564, 114550, 183997, 115833, 23481, 2828, 48124, 16340, 247, 309535, + 19416, 66487, 105120, 17809, 29656, 47021, 201858, 160017, 19280, 78447, 21406, 39940, 98734, 122438, 36909, 83000, + 7715, 134747, 24473, 75401, 18311, 9336, 15781, 55262, 30701, 9503, 53173, 8524, 133602, 90200, 85568, 13433, + 150307, 24618, 45822, 23920, 126956, 23395, 51943, 161287, 7378, 37763, 121912, 32196, 2933, 19681, 9297, 23283, + 79903, 162071, 55832, 23137, 13178, 209527, 9418, 18467, 12593, 119121, 83307, 51772, 86571, 52216, 23349, 61608, + 26604, 94854, 7575, 204746, 92446, 22189, 28677, 83268, 13574, 65266, 57786, 61446, 44205, 26661, 107681, 91716, + 65699, 82006, 71722, 70318, 427599, 64286, 24112, 101430, 21118, 144724, 36334, 80349, 8245, 44912, 45640, 4276, + 14407, 1378, 31347, 22711, 27877, 62250, 191999, 8464, 47936, 361021, 134866, 3976, 33542, 48109, 10852, 237219, + 7916, 122443, 17484, 70725, 26460, 109271, 187381, 128014, 117762, 246379, 22586, 82080, 21596, 9055, 4112, 313930, + 37818, 163471, 108924, 39581, 2249, 13074, 7431, 75393, 127359, 35494, 111020, 59720, 209394, 14051, 37123, 41719, + 60044, 39589, 172595, 143831, 47677, 59508, 24424, 103244, 308089, 7173, 30716, 90486, 36791, 60870, 101299, 108569, + 154971, 20715, 53888, 53901, 31417, 53959, 2728, 97125, 6998, 145934, 80463, 20412, 150042, 45242, 187078, 7592, + 174360, 31180, 135274, 14872, 12255, 11730, 93361, 83265, 43009, 49328, 20215, 21789, 29335, 115863, 28254, 39036, + 48739, 17655, 180783, 12531, 3912, 8728, 149240, 87661, 101398, 77276, 44669, 83920, 53340, 37769, 16150, 114794, + 2580, 157793, 7167, 176552, 146939, 5647, 16877, 13855, 151295, 90625, 68053, 91363, 45360, 176357, 42521, 30918, + 125275, 57667, 44098, 88883, 15273, 61531, 4145, 110202, 45383, 253407, 75996, 20238, 27456, 20964, 4620, 8638, + 48761, 12194, 52351, 99825, 23314, 86104, 80185, 11840, 8750, 45404, 19895, 71824, 60801, 13493, 12198, 37669, + 85823, 77592, 114135, 4393, 104759, 210654, 28711, 86484, 27894, 35912, 20614, 184536, 123685, 134244, 44348, 10862, + 66968, 165136, 18647, 32424, 39480, 60719, 93213, 164629, 26917, 39935, 75223, 75930, 55290, 22975, 122323, 10513, + 83305, 99747, 19500, 77541, 2696, 77867, 101290, 65344, 98390, 12816, 83594, 39244, 57569, 8468, 34263, 35071, + 145853, 12146, 76941, 17746, 340733, 25910, 37273, 43127, 4919, 60464, 1992, 49562, 20024, 15497, 109739, 14126, + 52268, 76976, 6239, 8384, 30884, 19155, 135974, 107147, 7413, 64546, 38363, 186999, 203685, 81795, 108201, 36022, + 70989, 30403, 116774, 5379, 112855, 28461, 9025, 58269, 4129, 17604, 104476, 174522, 50536, 1225, 10900, 36288, + 349518, 20856, 101414, 45229, 68205, 77322, 197569, 10875, 332641, 23358, 85554, 14077, 159581, 34721, 18791, 98700, + 135361, 167675, 50000, 42501, 236026, 75571, 5992, 17097, 37563, 20166, 101652, 40721, 176404, 19063, 36155, 59023, + 64899, 33196, 98793, 70148, 6578, 69024, 242235, 52605, 122033, 81904, 12809, 15158, 63871, 107782, 1840, 4872, + 1850, 29779, 34125, 98682, 85234, 116592, 82990, 12554, 82089, 63993, 168833, 149888, 66124, 10489, 196009, 53058, + 74145, 69106, 89715, 51982, 121098, 13559, 95458, 107427, 107351, 113368, 3064, 70286, 12687, 4839, 22981, 48279, + 36881, 55099, 198175, 73274, 117334, 75733, 3562, 5484, 13136, 64209, 184119, 61973, 14698, 16622, 73579, 36910, + 85933, 42774, 90601, 40132, 93866, 19937, 45703, 23982, 18047, 5362, 11140, 39687, 32620, 25714, 60265, 77976, + 310, 77325, 31712, 280974, 15094, 86261, 43015, 13789, 46000, 100529, 1116, 137536, 88451, 20938, 134125, 17791, + 23632, 63779, 256927, 29623, 36645, 219053, 83430, 48090, 27940, 27300, 14780, 109338, 87618, 34615, 102225, 42200, + 80520, 6619, 74506, 194075, 58892, 76807, 27201, 60732, 20976, 110501, 170773, 84913, 27702, 21417, 5342, 72467, + 140090, 178520, 122111, 33447, 96075, 4699, 53512, 17029, 67841, 174853, 17388, 16526, 103292, 6179, 27759, 37717, + 238964, 22906, 23675, 59996, 87778, 65471, 88269, 10094, 107338, 46577, 43856, 31927, 17019, 15, 159648, 72870, + 83427, 14710, 153353, 15418, 67522, 32362, 44785, 3937, 7302, 108965, 41424, 55752, 8261, 138626, 43498, 8777, + 47570, 24353, 207472, 131958, 116787, 3156, 34108, 58412, 8730, 6898, 90267, 28837, 80484, 59214, 9439, 19087, + 61772, 5722, 23159, 81837, 174422, 59842, 18102, 171770, 56005, 80656, 119313, 22496, 131193, 22339, 3665, 14366, + 52875, 107272, 15645, 70022, 14588, 34482, 10876, 36184, 28471, 60828, 82588, 55312, 59817, 102390, 8845, 87428, + 9586, 17179, 96409, 90979, 15720, 107181, 33777, 113545, 158155, 4326, 32422, 188604, 51307, 29601, 10067, 16201, + 109734, 3998, 129448, 141025, 15196, 18904, 83270, 17683, 11025, 19743, 21493, 1552, 59331, 7581, 25652, 89130, + 3884, 119328, 75611, 26479, 52626, 13590, 195115, 6807, 102602, 52190, 72507, 52467, 84797, 6467, 2334, 47785, + 25158, 132580, 142359, 76578, 27314, 24766, 3969, 27500, 4437, 254621, 65367, 168149, 20488, 13843, 62310, 21136, + 76214, 12671, 23644, 139645, 189248, 55341, 3302, 14123, 35023, 48936, 11475, 55049, 114952, 51698, 297764, 80767, + 157376, 20881, 13336, 53207, 2827, 59849, 16592, 52991, 62439, 275657, 47305, 117196, 102878, 60777, 83027, 10464, + 129749, 26707, 251779, 125019, 36405, 33132, 25562, 70233, 10329, 28189, 12385, 71977, 109339, 157513, 195426, 70901, + 108633, 55857, 3229, 108445, 36662, 75267, 19054, 102039, 1254, 2391, 173276, 29374, 13267, 51771, 225351, 7237, + 5470, 98738, 14316, 209876, 87105, 107056, 16157, 44140, 58004, 88474, 31907, 11831, 15397, 11396, 39700, 90254, + 10434, 316, 22682, 60529, 159667, 46855, 41392, 55643, 21864, 20640, 1483, 48142, 52022, 14052, 14307, 23845, + 179464, 105497, 8112, 170979, 3013, 22501, 58766, 9748, 32147, 168407, 69486, 20613, 31496, 139990, 50815, 4616, + 116832, 5525, 13988, 136236, 18494, 72230, 42950, 16966, 105502, 65693, 12439, 22036, 129227, 24413, 66758, 60339, + 107267, 12682, 60700, 199944, 50033, 64809, 169, 18106, 13481, 63773, 97251, 21927, 9954, 10994, 61753, 132964, + 24267, 269121, 315805, 17090, 22141, 89553, 60, 41720, 16257, 267739, 11800, 148430, 116154, 107691, 1495, 7110, + 36185, 30330, 15572, 9879, 950, 164278, 12417, 189181, 115685, 238527, 61115, 3001, 11305, 185925, 125509, 80597, + 176708, 23407, 47313, 198512, 2048, 137403, 10323, 39029, 178671, 3975, 76003, 84147, 112573, 78996, 69042, 6992, + 287867, 46759, 5275, 30027, 162328, 18790, 274565, 55200, 497663, 89305, 12413, 26108, 95170, 13648, 15748, 78466, + 50979, 40335, 43390, 12453, 193861, 466, 66161, 87201, 50987, 94748, 32988, 22018, 30368, 175641, 225636, 53920, + 136257, 72869, 274820, 12673, 31830, 21228, 5225, 4705, 46549, 105854, 3199, 16143, 15119, 48337, 134864, 57495, + 169876, 45311, 13298, 54322, 23788, 14950, 42143, 49801, 17462, 217840, 101953, 63412, 249887, 9473, 75292, 729, + 57377, 66067, 6712, 114078, 1949, 92149, 44063, 86259, 35448, 19529, 20598, 19485, 14791, 73304, 47309, 366, + 43769, 14667, 98883, 25339, 210091, 45717, 49613, 6739, 3783, 165357, 28175, 87394, 34612, 90151, 838, 17747, + 282103, 59822, 30329, 68354, 88380, 37787, 19846, 9765, 245190, 63635, 236, 10025, 5457, 17502, 12156, 204683, + 20491, 39054, 8070, 50911, 98908, 99317, 12186, 49875, 11402, 152980, 22961, 22075, 86899, 34073, 14755, 13282, + 117916, 33225, 72465, 158235, 16028, 64557, 36732, 123058, 162584, 20479, 31391, 63733, 60644, 62683, 9917, 17310, + 320177, 239647, 43989, 8876, 156096, 31699, 114739, 105603, 31065, 78174, 95969, 9220, 55876, 38786, 58411, 214643, + 22000, 140310, 88748, 26085, 77655, 28585, 109281, 7624, 9992, 7234, 98903, 60178, 23397, 75321, 53549, 53032, + 13757, 109032, 64081, 19046, 317623, 1946, 71713, 62867, 63978, 3151, 56924, 47304, 215255, 182040, 74590, 48012, + 2443, 24268, 69427, 33, 17648, 12518, 40668, 140511, 93231, 72823, 28791, 47052, 27388, 55205, 30906, 57350, + 104529, 63338, 26728, 100624, 93807, 107269, 82876, 163825, 55505, 18547, 30605, 14974, 140477, 81686, 69012, 120278, + 12046, 5605, 47923, 212928, 112040, 108335, 21336, 38303, 70887, 143240, 19855, 87583, 40152, 15587, 66375, 29394, + 94365, 54674, 43272, 133291, 112353, 210955, 121711, 331352, 25063, 35707, 69840, 56454, 114679, 66792, 86743, 446056, + 266061, 12010, 82324, 96915, 71248, 22037, 64756, 25091, 119555, 13915, 28628, 6675, 15589, 106123, 53880, 102126, + 2244, 55885, 39035, 5738, 617, 441160, 19553, 2220, 14129, 86275, 107256, 166687, 211431, 81495, 104573, 290527, + 70110, 99357, 25187, 33256, 100652, 31225, 142056, 82578, 7777, 23449, 58276, 64423, 4383, 192266, 134799, 66044, + 85911, 76876, 90974, 103947, 89221, 15961, 109118, 116095, 21010, 92406, 79525, 73542, 120615, 156252, 287243, 95281, + 58357, 14409, 38018, 33736, 86405, 83837, 55695, 9631, 37554, 27881, 106045, 6071, 41647, 35485, 108389, 46354, + 18, 34159, 180231, 126085, 15143, 13910, 144853, 93597, 69662, 64563, 23068, 38208, 60491, 41204, 62238, 141119, + 14714, 38001, 3276, 56189, 186134, 91982, 65866, 6904, 148344, 128140, 61946, 3683, 42347, 4969, 38485, 12705, + 5410, 78157, 91933, 103926, 168175, 15606, 288735, 6045, 44535, 20558, 34571, 13823, 42449, 6093, 11546, 37307, + 343894, 81874, 112517, 50738, 129417, 141962, 5390, 118181, 99682, 16804, 16660, 107424, 20659, 45287, 22461, 55188, + 27272, 10167, 12088, 17401, 140483, 17330, 78850, 45339, 63455, 16978, 199211, 80486, 222159, 13457, 1094, 18869, + 17536, 103284, 31050, 17962, 13722, 71867, 76368, 286932, 42637, 10567, 2, 199412, 62324, 36676, 8181, 6401, + 11976, 162, 87196, 78376, 114691, 1307, 30802, 3711, 148109, 74365, 57920, 81357, 2283, 214679, 12573, 18531, + 32057, 44583, 212119, 44264, 182393, 154286, 35753, 8258, 4295, 13186, 83291, 4736, 147364, 121944, 59024, 73352, + 33705, 31001, 88498, 32882, 2075, 163404, 10634, 11500, 44303, 65212, 13112, 34394, 30274, 20847, 64779, 22883, + 28331, 12500, 41997, 11945, 63740, 75992, 39703, 69806, 69740, 66087, 128693, 81128, 29148, 111583, 148435, 31535, + 10346, 159917, 58414, 28273, 44862, 59, 33364, 21068, 33716, 46470, 1804, 73963, 73937, 219049, 103305, 218321, + 153333, 233125, 63775, 105390, 12930, 127548, 60730, 109562, 38784, 53330, 627, 73923, 247159, 30199, 38362, 48641, + 2515, 19205, 34500, 48846, 41053, 21212, 105356, 15212, 20256, 3616, 21849, 17211, 83368, 97044, 352396, 108368, + 256189, 196693, 4226, 14922, 54639, 25114, 35849, 320, 115240, 41400, 60330, 23377, 5096, 75542, 3178, 56889, + 24661, 12431, 135214, 63314, 175419, 239496, 41215, 13379, 153552, 73270, 37517, 105147, 26516, 27776, 68242, 1357, + 141, 71029, 155582, 76053, 138176, 8168, 4134, 134075, 63885, 2519, 46027, 51951, 34115, 79709, 5576, 10203, + 47222, 15435, 34313, 43154, 55709, 9408, 82175, 88231, 2765, 52283, 66501, 65412, 28479, 574, 36643, 57430, + 38875, 293893, 16069, 28162, 236608, 26442, 8613, 1622, 12229, 37482, 74496, 33264, 22921, 79056, 39384, 129956, + 77291, 20463, 58349, 109725, 54426, 106360, 47581, 90881, 45388, 31487, 128197, 71530, 2860, 39582, 46028, 81826, + 57787, 259117, 67523, 307547, 114579, 104215, 18723, 115699, 295139, 4428, 43172, 26618, 105782, 14781, 30313, 18939, + 17826, 45064, 69594, 13204, 71066, 58366, 15231, 157682, 19119, 15787, 91984, 89607, 54364, 23607, 79019, 145509, + 69385, 6604, 40313, 81481, 16568, 32756, 79187, 77128, 12323, 56725, 270449, 8124, 28057, 84040, 65629, 72616, + 33346, 127078, 185998, 104454, 34919, 182273, 14609, 19947, 124763, 41923, 73459, 52863, 155533, 54653, 13742, 70467, + 101386, 30438, 9573, 50346, 31644, 14370, 50550, 451, 8627, 280645, 27470, 64783, 49001, 44841, 60011, 84880, + 303600, 29723, 249679, 20808, 29868, 17505, 9061, 24139, 63213, 37901, 19817, 58724, 9103, 3211, 42909, 2761, + 77280, 71424, 62334, 7307, 71333, 27508, 6331, 83663, 9696, 179679, 27464, 32253, 138789, 36870, 23709, 126072, + 37059, 130604, 83178, 283517, 24823, 22443, 72508, 24678, 5057, 22926, 13846, 74055, 21352, 14335, 3461, 51988, + 32368, 11407, 71381, 7895, 114208, 12515, 35745, 29717, 56803, 108507, 67583, 54834, 19424, 38668, 35778, 15261, + 10445, 67937, 56244, 3340, 58514, 22440, 11243, 68666, 8661, 40929, 111708, 37637, 209508, 124495, 206903, 91080, + 26187, 11633, 66771, 30871, 171838, 197948, 8289, 57706, 10460, 80625, 36923, 31612, 63454, 35105, 59184, 7032, + 14016, 40322, 27055, 86917, 122504, 79371, 130575, 6177, 41328, 5209, 78779, 4821, 21329, 41539, 62219, 83753, + 46618, 7613, 15027, 118360, 32493, 225969, 819, 120335, 38225, 84581, 17131, 15188, 7855, 3105, 30340, 78018, + 31763, 5801, 31461, 10241, 7945, 13882, 30152, 4527, 29876, 21438, 25462, 64433, 8734, 24877, 74945, 14455, + 6438, 107317, 52662, 50955, 24205, 109805, 38229, 13383, 97490, 25051, 32380, 7096, 139977, 101853, 1428, 42064, + 130740, 33630, 82143, 110070, 47323, 64635, 163313, 6594, 33195, 6607, 12008, 3521, 85390, 10031, 28761, 7750, + 57194, 65848, 171501, 76792, 13813, 65534, 81423, 13132, 60600, 4409, 31583, 87558, 21313, 106333, 155905, 153496, + 96251, 3406, 49514, 9434, 7699, 87327, 46359, 66114, 27584, 74162, 19886, 252277, 170521, 22092, 82253, 115261, + 139271, 47533, 52680, 202618, 1363, 11026, 363255, 99673, 4402, 33612, 20302, 28039, 336738, 1226, 38272, 13620, + 129223, 13205, 3958, 19963, 84983, 18752, 42515, 68608, 69150, 39813, 30549, 41506, 13147, 4080, 5087, 15836, + 3590, 70019, 16566, 11738, 163929, 27964, 83106, 22713, 207225, 34956, 53824, 5181, 155260, 90376, 13809, 102964, + 55916, 44273, 1261, 26537, 20288, 96003, 17325, 62927, 4503, 126513, 63919, 15556, 8398, 46320, 36814, 33274, + 98490, 42874, 152755, 42575, 11773, 27872, 33828, 220575, 27512, 172804, 16418, 95543, 37113, 708, 88986, 499, + 84976, 72480, 35781, 122997, 86558, 582, 113958, 53612, 28365, 4495, 11592, 12890, 11756, 81693, 108202, 42686, + 116005, 805, 108429, 99193, 182148, 10373, 83443, 91669, 13733, 56065, 75316, 204077, 115313, 322052, 15302, 23067, + 47644, 13460, 67103, 57307, 67208, 156322, 235278, 94136, 85069, 1418, 167462, 56633, 9347, 2641, 44606, 21287, + 14995, 12747, 17526, 494, 226141, 75964, 18039, 39483, 14704, 59304, 1316, 195645, 101835, 141208, 116613, 223219, + 41159, 49846, 61222, 43002, 35314, 80457, 23691, 63429, 13113, 36373, 32330, 8361, 63526, 11029, 32963, 93376, + 214039, 18769, 114006, 1087, 29978, 114423, 64428, 31774, 50446, 49295, 34202, 73765, 83339, 74913, 27287, 43834, + 17440, 83606, 93489, 60736, 129441, 57951, 84464, 118033, 72522, 2386, 5028, 276747, 118641, 55191, 48057, 16621, + 97816, 39006, 3084, 207838, 24907, 92233, 46363, 526, 73844, 2610, 25911, 60919, 15717, 85425, 136040, 70858, + 118884, 18348, 172680, 47190, 167255, 11777, 29552, 95596, 96509, 115491, 19228, 15022, 162793, 7055, 54334, 45726, + 30847, 5690, 94006, 54915, 36849, 132591, 172983, 1103, 51297, 9369, 114578, 16962, 78974, 112537, 1229, 44919, + 77793, 29231, 3317, 48815, 10427, 19768, 100349, 70835, 1873, 26539, 93804, 85247, 2972, 161694, 16704, 323037, + 9999, 22014, 49315, 152394, 35074, 21009, 44279, 9515, 28190, 8647, 79457, 53333, 64297, 16040, 25316, 4727, + 146836, 234761, 5304, 67997, 46298, 88700, 122826, 40595, 60038, 132292, 15881, 9158, 163007, 47825, 221096, 91414, + 108919, 98428, 57919, 266818, 61219, 43879, 63697, 17096, 2403, 17970, 81556, 16083, 75022, 59692, 100052, 43977, + 127339, 40049, 119563, 17907, 4233, 35651, 47435, 66632, 110389, 15505, 25565, 177269, 69022, 40996, 21600, 19932, + 9833, 15084, 84838, 139091, 128097, 47428, 7050, 1360, 88016, 32635, 67695, 9688, 79390, 38684, 76616, 96096, + 222936, 46245, 114060, 64752, 22570, 74272, 1778, 64322, 94657, 115063, 64956, 78933, 28462, 67295, 52520, 30887, + 56956, 34609, 42753, 10281, 38803, 27122, 98080, 117097, 81536, 9071, 86592, 83849, 30474, 44691, 14751, 101014, + 152794, 5331, 6917, 12448, 19566, 55177, 112969, 39493, 16481, 84170, 80483, 83854, 147408, 10964, 69929, 25567, + 74574, 33765, 31137, 41456, 81895, 68446, 44249, 35692, 20731, 44729, 68861, 17500, 1918, 8121, 12733, 11511, + 1366, 110805, 162699, 45509, 76367, 53890, 22608, 6608, 187321, 92727, 87863, 8920, 54494, 50474, 3355, 13177, + 24366, 3750, 4900, 105058, 21690, 24548, 61391, 41587, 61696, 71254, 21133, 115118, 33283, 43646, 8976, 11273, + 107477, 5228, 145257, 193829, 77499, 89840, 14294, 15416, 31112, 552645, 70476, 18585, 414383, 91301, 197650, 85075, + 74362, 201919, 31008, 6857, 18463, 18747, 80565, 258617, 218441, 23173, 30916, 2323, 120929, 143751, 107064, 20153, + 59848, 60391, 289834, 31449, 258629, 45824, 75786, 49114, 201924, 23563, 35265, 68991, 69269, 10313, 17390, 67981, + 454, 162917, 7572, 144990, 19989, 55673, 279099, 65066, 13054, 54343, 55277, 3568, 59894, 15271, 53443, 417392, + 3623, 434, 45943, 1256, 58908, 173322, 64122, 81605, 20681, 59366, 25442, 39040, 35723, 390351, 35866, 146738, + 78523, 61914, 82593, 35139, 102680, 249269, 46578, 8727, 38988, 4624, 48520, 58695, 184112, 2027, 12940, 80034, + 108087, 27491, 213336, 944, 50944, 22396, 85571, 42349, 132704, 181305, 10849, 1371, 52966, 36306, 27461, 21214, + 21699, 5289, 15632, 19188, 18860, 17496, 112885, 63208, 96349, 12436, 34610, 13227, 201411, 150107, 21944, 52213, + 82697, 377613, 11201, 44987, 85395, 150187, 38121, 21649, 95658, 8788, 60763, 9623, 5093, 194516, 47673, 70064, + 6427, 97874, 32248, 15210, 177894, 45245, 129556, 10575, 44191, 36758, 23545, 141857, 32755, 22539, 162901, 95737, + 26961, 59210, 28436, 11081, 155739, 67439, 17384, 48388, 6249, 69756, 55378, 34764, 31310, 113162, 97192, 14330, + 81030, 18838, 35438, 13932, 26574, 120400, 101846, 130645, 84311, 174890, 23927, 28384, 120155, 7519, 67818, 55584, + 86730, 120665, 55022, 167671, 113535, 159014, 209287, 66396, 7424, 10800, 50330, 93120, 48888, 11155, 16081, 102627, + 13516, 33788, 137813, 74841, 45747, 35841, 21356, 6047, 98098, 4647, 73346, 95355, 20077, 37795, 143033, 12452, + 183995, 86627, 142356, 34744, 81945, 11420, 26913, 10645, 43210, 5817, 54484, 55730, 26704, 91611, 17668, 52476, + 40420, 51581, 44960, 150119, 75831, 205414, 177424, 80186, 45648, 15047, 63973, 15971, 11180, 3401, 50574, 45327, + 6855, 127, 294368, 30972, 57927, 224035, 22536, 5728, 65528, 17692, 2498, 20700, 124096, 78896, 81810, 53646, + 34851, 17737, 67858, 144208, 2598, 69506, 82387, 32829, 156633, 1376, 87537, 38787, 107572, 47042, 34484, 111751, + 127352, 97800, 89687, 4576, 38169, 10102, 247669, 1385, 123845, 61475, 17048, 8281, 60142, 15760, 189275, 5043, + 62722, 70423, 23446, 224950, 105584, 6317, 280103, 26981, 232364, 55457, 64267, 136900, 23211, 118951, 845, 54134, + 68120, 21238, 286023, 351719, 1601, 9442, 185113, 17851, 22169, 76287, 138957, 35304, 89299, 75522, 56601, 68253, + 747, 100548, 4081, 2246, 258039, 9694, 51725, 15738, 80572, 37864, 139906, 92854, 7258, 4446, 72431, 8572, + 152249, 3376, 43649, 25854, 11862, 510329, 182162, 28232, 101204, 130724, 77756, 137694, 8834, 65401, 19289, 56727, + 121434, 112217, 15745, 51403, 33761, 31323, 43153, 136892, 19175, 85899, 25269, 38059, 133142, 52989, 1777, 1223, + 46343, 18613, 28695, 87511, 40178, 223203, 15033, 135709, 48723, 7442, 84744, 52715, 3589, 26620, 71258, 37458, + 41977, 8646, 41763, 91920, 30210, 33735, 64017, 14173, 38868, 120841, 22212, 152812, 62257, 17607, 2045, 16253, + 10087, 16559, 33187, 61003, 82658, 60687, 45996, 131329, 87827, 28306, 207690, 248902, 90646, 20868, 109813, 9386, + 16415, 77631, 94695, 12715, 47552, 53324, 61324, 84409, 351723, 34328, 65573, 16236, 28298, 8448, 4731, 7418, + 72225, 87876, 82665, 6012, 91146, 32182, 85776, 95303, 272760, 52003, 18667, 6217, 1701, 78472, 2900, 125100, + 11295, 44716, 79152, 6181, 1652, 8164, 31057, 77402, 109651, 90467, 194601, 7484, 300747, 17565, 61525, 99466, + 51863, 9753, 72732, 128898, 198800, 907, 5759, 42875, 29446, 20325, 107060, 165430, 11794, 312, 5807, 20106, + 32345, 138270, 25904, 39695, 37538, 97877, 31477, 37750, 22356, 11523, 124253, 73049, 33102, 10210, 142709, 60863, + 37590, 128516, 64551, 64303, 113544, 313515, 181738, 31113, 37970, 55419, 66610, 21171, 11478, 7770, 228067, 47156, + 179743, 798, 147431, 50652, 25454, 19412, 37683, 4903, 103417, 111432, 203788, 3756, 59905, 35566, 26345, 21937, + 221970, 77922, 9738, 6303, 105196, 61175, 21598, 10726, 145604, 59463, 162519, 29420, 7817, 116768, 16102, 128966, + 164809, 62009, 21801, 6296, 102360, 37967, 27376, 20294, 16974, 106654, 129570, 55893, 75840, 88896, 68684, 52692, + 255333, 13102, 29474, 23608, 56902, 95016, 21221, 14815, 6659, 24894, 19248, 23158, 1954, 30789, 75920, 14310, + 645, 139270, 54809, 114378, 59400, 18159, 138353, 504, 67769, 40337, 187608, 1166, 7689, 50368, 74658, 121130, + 18675, 55085, 92265, 193168, 5215, 27373, 68051, 32611, 13793, 36800, 37117, 18010, 20536, 292596, 68283, 17340, + 27852, 177093, 366025, 172, 3387, 59699, 119854, 49684, 29523, 615, 8943, 120515, 259718, 38781, 65830, 116121, + 16860, 30836, 32413, 2567, 94625, 94075, 101496, 152484, 43143, 15550, 8257, 43438, 29245, 8300, 137692, 67582, + 15848, 41480, 7018, 78578, 233581, 95833, 94811, 680, 22685, 16034, 79357, 33983, 63631, 172275, 30827, 99510, + 78557, 35720, 123406, 2852, 22836, 77392, 107120, 19910, 133302, 7738, 237843, 59764, 84513, 10802, 76294, 191768, + 1348, 1723, 835, 44328, 51826, 15011, 147582, 11622, 47129, 100405, 10694, 21708, 98836, 9941, 210066, 1364, + 58284, 76266, 30451, 238107, 1830, 218, 64337, 500320, 1749, 129373, 99321, 63068, 94642, 60327, 22930, 25615, + 10933, 11881, 32269, 125198, 6145, 6468, 88776, 9729, 12506, 25930, 46197, 67476, 10975, 30498, 22641, 6110, + 13879, 228714, 189675, 30112, 103781, 142137, 12074, 72148, 144434, 17332, 1205, 55723, 10268, 84346, 739, 7900, + 28409, 142772, 86335, 34405, 32346, 5957, 99727, 159495, 52968, 69914, 49976, 116713, 121567, 45867, 1035, 114241, + 107374, 16112, 22042, 28431, 77268, 110960, 20030, 87679, 23686, 164921, 11944, 46255, 35097, 13908, 12674, 144017, + 10501, 8182, 68576, 13277, 155275, 111289, 84842, 25219, 15303, 77277, 85173, 34541, 47136, 169489, 134845, 66303, + 21102, 68903, 56191, 22964, 168741, 12959, 7590, 12803, 55332, 44576, 35459, 106903, 90385, 5415, 140923, 1080, + 15996, 137579, 8958, 18617, 84817, 54000, 41832, 10520, 681, 67009, 192636, 36305, 137803, 34054, 76848, 21244, + 25054, 15186, 30898, 30597, 142275, 43375, 42593, 28736, 6163, 62732, 73310, 84072, 38175, 49734, 9130, 100431, + 8056, 31178, 23491, 108251, 124296, 54748, 48773, 67591, 240642, 30480, 55118, 280935, 65621, 36603, 32046, 51350, + 4934, 31903, 121291, 176064, 178205, 68901, 29505, 71720, 16101, 99503, 28716, 1623, 62803, 17988, 139992, 3625, + 60964, 35799, 20217, 10717, 18230, 122058, 97858, 147682, 100622, 42915, 18203, 67908, 76465, 68188, 84589, 230422, + 44689, 25437, 16646, 1939, 14545, 62476, 16816, 55294, 9543, 86343, 30341, 131304, 47514, 35443, 171825, 56555, + 16852, 117154, 33897, 115642, 93380, 27861, 5302, 4455, 28048, 30630, 34058, 166130, 12047, 143095, 91785, 86862, + 107106, 33521, 3018, 20571, 37575, 98074, 42118, 36747, 101485, 21122, 7995, 35412, 77047, 174662, 44717, 27754, + 57326, 45416, 100544, 40237, 34819, 147882, 8009, 40696, 96137, 22034, 12537, 89019, 76916, 16381, 260347, 20562, + 6469, 99814, 6504, 8878, 46264, 28993, 135328, 351396, 115983, 162985, 9453, 27223, 75768, 129072, 94641, 114365, + 87668, 163393, 133224, 129378, 69942, 52468, 102771, 87719, 13027, 63666, 4880, 269088, 165, 102664, 209256, 8459, + 8373, 115431, 33913, 89243, 114231, 39718, 48970, 84535, 26434, 89763, 13534, 52518, 52844, 4523, 118415, 84250, + 42799, 85012, 51296, 129398, 182044, 164191, 23430, 172125, 23580, 17068, 100097, 165599, 146254, 103774, 23886, 5517, + 38081, 6556, 39590, 7165, 43236, 9564, 11470, 79503, 33883, 6422, 32630, 507, 146220, 14032, 54871, 27774, + 382894, 111499, 100220, 85905, 14606, 67028, 127208, 96410, 46035, 45103, 89938, 72023, 36481, 10463, 84711, 2184, + 166621, 34621, 71767, 6005, 35417, 18397, 12328, 6245, 95382, 74094, 6382, 42236, 2957, 19896, 58715, 27397, + 59384, 32742, 2546, 93465, 60428, 48668, 29303, 20252, 36358, 149322, 3193, 36701, 66343, 73618, 24279, 42201, + 75378, 214932, 112583, 62412, 22267, 87725, 1442, 11196, 22950, 229298, 116569, 171221, 83528, 29153, 38927, 69087, + 17577, 73532, 64199, 281267, 56474, 107520, 35241, 9230, 25285, 16464, 54961, 820, 4619, 89654, 59498, 85252, + 179691, 2434, 26357, 9414, 75355, 113594, 58673, 61757, 95836, 68318, 54415, 28188, 53295, 26258, 1129, 9855, + 34588, 10951, 29459, 270568, 171410, 122449, 84225, 40183, 4487, 93745, 119118, 40504, 14679, 59040, 261948, 112143, + 84208, 36024, 77263, 53688, 44015, 109308, 151516, 53139, 18562, 27509, 122, 78679, 109133, 27618, 9307, 11687, + 54101, 37528, 73589, 23837, 11531, 15405, 27569, 34097, 86052, 88898, 452968, 106351, 174479, 16086, 12337, 33522, + 303157, 114465, 20251, 93451, 28095, 29793, 32562, 4865, 9953, 17465, 73959, 11715, 35642, 25146, 120955, 73779, + 14564, 70652, 194775, 127647, 39802, 8232, 15171, 32361, 16145, 146754, 54118, 51290, 77606, 61329, 94469, 20733, + 117406, 168386, 13963, 5919, 53038, 27673, 29890, 70790, 121117, 125451, 109176, 9417, 53624, 27539, 61759, 90921, + 22062, 12907, 8858, 21259, 1212, 82256, 83446, 126781, 7632, 49742, 4924, 99538, 127157, 121931, 106183, 86599, + 237292, 72098, 87000, 4747, 189087, 63407, 75261, 28941, 10478, 97792, 6128, 23277, 127345, 38013, 46884, 1492, + 102515, 12723, 3978, 16950, 181997, 58167, 67025, 121398, 86752, 24702, 76144, 13670, 87623, 143137, 34358, 58216, + 10966, 15260, 79095, 10693, 121602, 149602, 4024, 88047, 68783, 112717, 12525, 7881, 68681, 9491, 38696, 70997, + 83042, 12348, 7661, 10266, 114380, 152092, 105889, 5453, 138349, 17021, 6772, 17024, 191305, 9666, 12566, 121832, + 67176, 6357, 20860, 13799, 50085, 58837, 33490, 31519, 39016, 25779, 4404, 75055, 1427, 51784, 37042, 40467, + 42384, 128165, 26665, 15628, 1412, 114798, 10282, 15463, 67118, 108331, 37941, 49355, 122616, 11721, 123403, 55963, + 72389, 45854, 157529, 86342, 25260, 17774, 101303, 13857, 2237, 12444, 17435, 9912, 13576, 18041, 9931, 109677, + 137346, 2419, 5631, 153579, 19938, 7345, 26359, 32429, 20304, 111232, 63629, 62874, 2191, 41851, 33540, 17568, + 68759, 206421, 20421, 8918, 5373, 10979, 52747, 28961, 61364, 35687, 21405, 9253, 238507, 56648, 20973, 119843, + 75814, 58786, 192, 23147, 23931, 351555, 317690, 34008, 69565, 12383, 22156, 37312, 38993, 98373, 54103, 108574, + 131741, 105495, 23252, 10516, 38364, 418691, 1022, 31867, 12528, 57830, 10554, 28925, 87762, 73786, 45238, 3871, + 5679, 49309, 1866, 15962, 129853, 7444, 10743, 34937, 5310, 18691, 186923, 15486, 186831, 78192, 56399, 34869, + 32653, 116247, 7899, 91437, 90338, 131084, 26080, 707, 260176, 152285, 65289, 108488, 389531, 49652, 28839, 250236, + 108118, 976, 52394, 48642, 26843, 1434, 7539, 14145, 43985, 32680, 56851, 16604, 50175, 15484, 133420, 201965, + 30563, 171323, 10408, 5106, 25106, 3832, 22052, 30595, 56965, 126120, 10162, 79454, 18130, 125828, 52128, 6763, + 140428, 50288, 3359, 75168, 4542, 67789, 21815, 25917, 165503, 152273, 62307, 3497, 117991, 32786, 40567, 896, + 24219, 83611, 40133, 65241, 229605, 317863, 103959, 90507, 1819, 29545, 15591, 32868, 129663, 93792, 44783, 13032, + 1240, 5795, 26358, 21931, 3797, 229975, 8527, 45899, 76093, 223890, 32636, 329905, 18398, 40622, 43735, 5700, + 71339, 108607, 70873, 19592, 51919, 16423, 4832, 81475, 93043, 49045, 105473, 57292, 27175, 161447, 88028, 82073, + 47060, 1862, 6336, 34528, 216257, 49542, 1523, 13933, 6483, 12498, 143270, 12904, 35051, 32513, 38643, 137631, + 1217, 10305, 15169, 34487, 16512, 91140, 101318, 54337, 80798, 39094, 72004, 164938, 129064, 45885, 110177, 68422, + 13225, 211848, 134863, 65992, 69339, 9421, 7754, 97572, 8548, 19362, 46011, 26566, 237079, 65134, 52432, 108604, + 72298, 156269, 99270, 75206, 2575, 146135, 49731, 81094, 34280, 39812, 5510, 134730, 51379, 103016, 61853, 94676, + 117910, 64, 7934, 95122, 55671, 3205, 58333, 173791, 53345, 20418, 92151, 39998, 247552, 69993, 40897, 8758, + 29486, 48394, 44428, 19361, 39328, 12796, 110850, 20923, 140821, 98505, 27484, 87170, 34681, 8696, 6881, 178272, + 57045, 10428, 21956, 22625, 60177, 192367, 30337, 97408, 5004, 22803, 112181, 36456, 90269, 45503, 47341, 44581, + 78522, 63489, 59424, 114517, 2479, 35959, 56238, 267536, 322607, 86378, 13479, 1160, 48474, 6182, 17033, 29824, + 61296, 57555, 82202, 35730, 13057, 15270, 72386, 73223, 31558, 45570, 36450, 117667, 4678, 14390, 62433, 28769, + 59271, 15277, 15374, 25119, 6699, 9480, 26668, 33155, 27044, 64427, 178, 36692, 31988, 5800, 18108, 209160, + 35944, 71154, 35918, 167895, 12503, 1771, 16989, 56793, 83480, 39460, 212938, 8159, 4389, 5594, 58690, 40182, + 136508, 1899, 67361, 119842, 3781, 42329, 32531, 37520, 114121, 97367, 52218, 49794, 70279, 62339, 116961, 90247, + 4488, 183020, 1828, 43025, 155829, 86696, 4847, 194990, 42214, 37848, 22958, 11578, 2898, 43626, 50811, 14189, + 68191, 47328, 4117, 163237, 75695, 118135, 64031, 37784, 305850, 99832, 84067, 112381, 45041, 38270, 15120, 240636, + 74344, 6693, 36080, 91318, 106509, 219791, 38255, 7554, 30087, 101966, 6713, 51615, 17429, 102583, 207618, 156, + 93292, 28386, 62288, 34520, 12477, 25385, 50776, 72166, 290, 7825, 17764, 23980, 23080, 74965, 22645, 203791, + 114802, 45119, 156694, 216, 35714, 42020, 117883, 147910, 18751, 59077, 92531, 63106, 26554, 23194, 21689, 105051, + 105424, 2395, 22296, 6139, 17775, 18788, 78110, 84290, 2144, 18630, 17328, 34740, 2412, 84397, 47585, 9344, + 100610, 186751, 213784, 22382, 65192, 94882, 115271, 162177, 113975, 16918, 4108, 47999, 52975, 14387, 51692, 40643, + 180272, 25467, 117999, 146655, 135050, 1246, 11255, 45090, 129815, 13155, 69900, 30778, 76238, 51833, 50960, 175019, + 106483, 105526, 121590, 62888, 21440, 37725, 6971, 74563, 63186, 4385, 6132, 28738, 4260, 7667, 208949, 44659, + 46189, 4885, 2107, 8295, 9711, 33755, 18758, 47913, 28249, 76357, 33947, 20500, 4169, 156995, 46268, 30937, + 23429, 30285, 10916, 52362, 23390, 2897, 68316, 35544, 8324, 51699, 132392, 78433, 141585, 44776, 84808, 4462, + 63809, 59925, 104776, 132583, 67668, 101503, 35715, 119985, 38457, 47365, 56836, 187538, 38063, 17773, 101921, 26244, + 39226, 27892, 7402, 164108, 59972, 87376, 323527, 169672, 1189, 114898, 48985, 100235, 203916, 95, 21963, 6130, + 62368, 812, 54658, 54753, 14403, 18099, 9204, 80661, 16949, 16703, 239430, 18692, 61767, 58048, 87844, 125829, + 85801, 177571, 8915, 209065, 1739, 136288, 19402, 9552, 40147, 51786, 57185, 93178, 35049, 55789, 6563, 35663, + 76757, 32960, 20303, 80517, 33124, 173105, 7464, 51755, 62102, 25470, 21379, 115520, 15780, 117655, 47580, 25076, + 103593, 140889, 141531, 170341, 103009, 155483, 236319, 50667, 53484, 1496, 133657, 34260, 22952, 46940, 6610, 65362, + 67973, 155084, 121252, 1114, 114645, 46684, 81892, 70092, 6566, 122003, 120281, 934, 5245, 88201, 9873, 128289, + 50462, 186108, 122319, 93482, 7601, 179944, 197940, 172496, 8288, 2228, 103865, 63562, 3513, 16971, 44832, 8458, + 194571, 18586, 21840, 94414, 80276, 279833, 284818, 117971, 1908, 62558, 8177, 5618, 54592, 34875, 38914, 80151, + 5124, 5315, 93652, 14782, 58571, 5164, 45076, 67898, 2513, 49234, 56032, 253677, 6800, 85426, 46580, 21278, + 273997, 56465, 57545, 120308, 193904, 19317, 27718, 67291, 1119, 22758, 15825, 65236, 17991, 81823, 6750, 11437, + 117245, 9177, 63553, 197299, 2508, 97808, 35037, 55401, 129156, 37796, 50612, 379962, 82366, 53364, 48505, 19654, + 26278, 19171, 20254, 13206, 71465, 142569, 27518, 4583, 63341, 506954, 106046, 14062, 56943, 1023, 4159, 99329, + 39662, 25053, 62754, 40102, 106116, 281486, 54055, 90158, 94966, 45145, 149583, 64918, 156875, 121779, 78151, 69085, + 9736, 35396, 9131, 26784, 2204, 8896, 115808, 87466, 122308, 77182, 75953, 30431, 94418, 20569, 21625, 175, + 27134, 19042, 38016, 95890, 1280, 29709, 5584, 120804, 24539, 41701, 33794, 262040, 49022, 72657, 12577, 34963, + 45314, 122802, 119410, 33620, 3764, 35300, 297940, 221821, 50904, 31890, 33498, 679, 46424, 24998, 9549, 17126, + 30699, 3402, 46901, 107591, 28087, 48118, 17943, 10684, 293839, 72628, 116809, 24752, 9400, 43553, 51404, 46957, + 33646, 107115, 59781, 66313, 40165, 60892, 19582, 17483, 822, 7964, 22063, 57161, 147499, 47679, 50833, 9574, + 50263, 102698, 56893, 336655, 116179, 23193, 11473, 52072, 29085, 2120, 164190, 67171, 11863, 14812, 284781, 18622, + 31314, 107961, 27863, 20352, 5578, 15425, 16772, 6733, 17797, 45919, 177948, 52326, 5104, 101129, 118088, 134374, + 12454, 238986, 175627, 243562, 1604, 162707, 28340, 5806, 15342, 103576, 54301, 104313, 219206, 110037, 13405, 101976, + 10232, 224569, 40292, 169439, 67800, 219782, 23823, 58633, 94261, 4775, 217506, 19292, 25872, 158455, 309664, 54744, + 13565, 217649, 145860, 19153, 90339, 68476, 52350, 11091, 78971, 60510, 6347, 95417, 75377, 62774, 238643, 45079, + 26649, 121377, 31224, 120575, 41184, 92773, 38918, 55073, 47695, 123557, 42923, 124821, 11514, 104093, 123806, 114651, + 35369, 65591, 36023, 60482, 20767, 40839, 12635, 10552, 14227, 109847, 53556, 40945, 41953, 4497, 82444, 74167, + 309396, 11706, 101696, 39123, 148270, 9459, 44262, 20639, 147938, 46790, 89576, 30166, 33074, 58222, 75050, 18854, + 14453, 79479, 32094, 56822, 27499, 8684, 27993, 15551, 109019, 360070, 45828, 143786, 39018, 134316, 51248, 33214, + 25738, 56409, 48044, 5319, 240196, 56286, 31884, 103159, 158931, 46088, 113738, 5489, 52820, 62007, 122208, 50310, + 8612, 192204, 14081, 25757, 95853, 28293, 144772, 72058, 21524, 28179, 52909, 10275, 137010, 29363, 50509, 211571, + 84901, 15663, 36633, 159109, 70869, 102773, 15903, 250288, 70021, 118, 136728, 103089, 116794, 6302, 4006, 31162, + 48404, 98849, 21638, 10331, 38771, 92331, 17759, 19077, 6732, 77161, 31843, 1947, 1070, 61852, 19400, 133326, + 70990, 71182, 53741, 56003, 187297, 38802, 52928, 47866, 49140, 42448, 132456, 52558, 5238, 95978, 34737, 124934, + 576, 52602, 99772, 2211, 3564, 107992, 7854, 37619, 253975, 33502, 97130, 7194, 16027, 44625, 176640, 70109, + 16483, 69386, 84445, 189507, 2811, 2911, 42607, 29686, 37775, 48856, 7024, 32934, 19034, 21522, 16906, 106835, + 25259, 237775, 26587, 28815, 4053, 8604, 55942, 126681, 2000, 45110, 26993, 6934, 70083, 18404, 97123, 242951, + 95774, 28200, 35245, 126075, 19713, 54541, 26771, 41892, 33431, 6278, 43932, 28670, 92703, 22776, 5711, 39966, + 91314, 25582, 90115, 25964, 42381, 57282, 8595, 60098, 288770, 91568, 25698, 84737, 48194, 43690, 3154, 56885, + 95985, 54802, 176675, 182814, 3991, 18462, 12526, 110444, 77418, 13087, 68801, 15335, 13406, 7781, 13313, 67395, + 241328, 90575, 70480, 44887, 245086, 30235, 7491, 81635, 56533, 280976, 128226, 12240, 35275, 20723, 261812, 18827, + 62725, 32865, 19772, 70441, 9246, 31671, 4690, 16830, 51924, 18082, 12155, 52604, 70181, 56355, 9343, 45521, + 95331, 89906, 123743, 26046, 16163, 794, 32226, 38434, 31410, 124030, 18573, 21412, 79016, 51513, 69499, 483, + 39312, 5830, 39528, 16907, 120878, 146499, 169663, 2790, 119371, 14322, 112030, 52038, 275987, 14651, 179020, 32135, + 80124, 13936, 24740, 53966, 27712, 86684, 12262, 6516, 9186, 18255, 29820, 140583, 220, 99643, 29371, 88024, + 23598, 104635, 329921, 84019, 146167, 73134, 35547, 58064, 85209, 47039, 3925, 35890, 68238, 152444, 111395, 26790, + 282190, 22569, 83839, 77937, 57048, 132950, 95253, 79911, 31273, 2689, 38285, 60446, 30555, 86725, 98060, 37473, + 80913, 23265, 231272, 356070, 17594, 117027, 84460, 79300, 75779, 181354, 106863, 2092, 59160, 174624, 41803, 31994, + 135002, 52912, 60077, 41514, 101219, 16751, 143391, 28891, 189377, 73103, 110543, 14827, 29225, 18801, 72180, 15635, + 96735, 20123, 8653, 54778, 60126, 24504, 72006, 22210, 62522, 119650, 6858, 20841, 104000, 97893, 37534, 99349, + 27620, 55847, 127814, 128781, 86814, 56767, 22957, 8365, 17240, 17411, 175172, 58526, 147533, 178431, 17806, 93628, + 11001, 10386, 11849, 45857, 5425, 535, 4471, 45522, 43682, 11326, 69505, 150707, 410, 109497, 103689, 22109, + 49460, 340469, 41561, 30428, 87270, 69036, 297676, 195760, 69480, 40499, 5201, 173640, 46315, 3997, 90953, 30510, + 59448, 243249, 347379, 83361, 1816, 4707, 31440, 4789, 76201, 1555, 28507, 130215, 9431, 5428, 7565, 108357, + 11788, 21925, 7664, 17680, 87960, 37326, 70316, 157989, 29063, 16049, 114035, 14852, 65539, 7305, 58413, 1877, + 47347, 17700, 121084, 175922, 11678, 14590, 26324, 40935, 33846, 20487, 2960, 82286, 7008, 140230, 95020, 131093, + 196704, 19819, 214983, 15557, 9895, 61765, 2774, 23793, 6753, 15070, 23136, 103206, 8633, 6988, 6539, 24357, + 120892, 28654, 43247, 90291, 59970, 66616, 39279, 57954, 572824, 154282, 103289, 83945, 115934, 30357, 45667, 105824, + 6646, 68979, 29029, 11138, 202559, 233721, 58053, 123432, 892, 61869, 24559, 4513, 48351, 9869, 14927, 39859, + 37611, 43939, 23103, 59284, 251282, 8496, 98455, 54160, 57823, 36455, 13031, 54764, 67263, 54173, 12367, 186498, + 57750, 24251, 33450, 28263, 26527, 3389, 48830, 62567, 34485, 22568, 19435, 87958, 90747, 51443, 111255, 9133, + 7685, 14251, 4357, 34121, 88370, 40692, 44187, 15871, 6144, 133877, 39726, 20248, 64182, 49145, 10818, 50541, + 1709, 19747, 172835, 1815, 41969, 4563, 131019, 10784, 21458, 34631, 34642, 27451, 62327, 10502, 64817, 21040, + 181657, 3356, 25303, 57684, 49247, 69433, 4964, 30900, 225330, 137978, 66824, 79436, 122600, 89064, 2877, 43195, + 114574, 122442, 100287, 41921, 107124, 25840, 145878, 21565, 85361, 235410, 5467, 28770, 111833, 74398, 71263, 46983, + 63243, 138675, 140, 48949, 71420, 26887, 24503, 148456, 15655, 33138, 34734, 7409, 191178, 14, 9258, 150881, + 72430, 90902, 102763, 36586, 18063, 309662, 7191, 149392, 51425, 83681, 24587, 13296, 54002, 210523, 16626, 112474, + 12364, 63416, 36624, 26506, 53225, 21831, 31787, 34, 86557, 998, 49738, 86372, 18193, 23048, 18612, 8630, + 97580, 38441, 26833, 25418, 41232, 78390, 16663, 190608, 138398, 35615, 30762, 41936, 67821, 38100, 49578, 39714, + 128724, 43474, 548, 16126, 8944, 23862, 54057, 149293, 233212, 265993, 19560, 43377, 101353, 40854, 29299, 30997, + 52099, 84221, 76059, 31238, 42127, 24282, 80418, 19725, 14006, 28929, 62417, 365698, 120107, 2583, 165916, 29207, + 32789, 70495, 14426, 38831, 32132, 4652, 155719, 175722, 3498, 105465, 149732, 73487, 18123, 17955, 25073, 7848, + 33758, 212117, 244871, 8698, 56058, 78012, 176282, 143192, 5779, 143466, 322846, 64979, 128760, 44417, 7522, 81933, + 59888, 57516, 87550, 22759, 98869, 19786, 24780, 17277, 18445, 71895, 46515, 17698, 84702, 95788, 12963, 2088, + 51911, 34015, 139375, 18024, 13234, 31480, 33157, 4088, 218379, 20152, 163491, 28846, 20093, 78589, 10362, 134905, + 39031, 1665, 31913, 27636, 8074, 44238, 29175, 3880, 70195, 32932, 86265, 35934, 20708, 49901, 110247, 9629, + 23462, 71547, 25903, 28239, 24355, 4901, 138539, 79668, 131384, 12295, 92413, 90944, 60189, 41168, 22999, 20952, + 26390, 12629, 62314, 162675, 10403, 221931, 3072, 11764, 41060, 197102, 26233, 17299, 7140, 4, 23363, 5513, + 10781, 164646, 18296, 151517, 49410, 18780, 28010, 94985, 42261, 103149, 80050, 34361, 87202, 65486, 9245, 50882, + 82566, 8189, 29402, 49903, 41663, 200708, 115537, 287472, 43105, 88272, 5751, 34285, 60276, 72903, 27444, 11115, + 2768, 40213, 17769, 32834, 5733, 17012, 31302, 65627, 74176, 65762, 220572, 9443, 28329, 49080, 12480, 25022, + 2297, 13079, 51120, 130422, 145430, 108651, 137217, 132944, 131632, 45724, 62952, 163637, 83615, 68196, 126016, 32802, + 122915, 52473, 182224, 14962, 105441, 51662, 20871, 19690, 655, 19897, 140506, 6328, 224102, 127751, 71453, 69604, + 5284, 37207, 87714, 44437, 136426, 34177, 153918, 197294, 67763, 66574, 112376, 9811, 16294, 44836, 174988, 254354, + 188511, 2252, 79267, 9326, 32538, 195, 43583, 8887, 61049, 21719, 26734, 11871, 27893, 192270, 45063, 30090, + 3394, 20502, 9111, 20810, 13951, 78445, 8154, 14291, 159099, 36402, 6237, 37096, 28542, 24459, 73277, 130650, + 17930, 20147, 11211, 3687, 145360, 109503, 113181, 11464, 9492, 132383, 106568, 69961, 190122, 21790, 110379, 8476, + 32285, 32541, 17180, 45498, 78855, 35803, 91050, 89637, 26440, 3026, 120111, 12708, 13570, 45990, 40699, 99704, + 58648, 92464, 151437, 6667, 73908, 1742, 3256, 33631, 4239, 45909, 170962, 320179, 124561, 64828, 19263, 102210, + 2444, 89057, 81206, 34535, 74172, 132424, 73378, 52206, 53131, 11865, 27025, 47218, 11468, 17209, 232859, 291876, + 10794, 27550, 80459, 63364, 73566, 231061, 98585, 163639, 11623, 81336, 209888, 37838, 35343, 51998, 144543, 217838, + 64710, 105694, 183409, 44047, 30481, 27765, 3985, 96950, 4163, 39768, 18053, 21140, 10328, 45473, 19463, 485, + 38309, 119763, 13419, 18090, 29901, 52576, 186587, 4918, 10538, 151025, 65973, 352, 154377, 192678, 90225, 68987, + 76132, 45679, 99334, 22260, 92405, 1735, 27185, 48079, 24839, 129186, 20096, 21327, 11679, 132020, 9799, 26969, + 3465, 457, 58718, 36333, 13449, 152147, 4317, 24297, 11637, 32905, 8133, 40341, 7824, 26227, 23026, 45709, + 2337, 33165, 12703, 29202, 57754, 193021, 33150, 59750, 1260, 52775, 3601, 206, 14458, 29558, 61780, 10854, + 41118, 3988, 39264, 86434, 19878, 45295, 164578, 92891, 38661, 105334, 321632, 27923, 13416, 145240, 66977, 20857, + 159180, 21051, 75795, 119241, 37074, 56804, 62179, 4633, 163164, 62782, 27211, 8479, 54137, 80694, 29848, 30457, + 28627, 13295, 19382, 20644, 52134, 10036, 217, 6449, 184900, 22218, 8094, 95155, 8520, 119873, 16010, 180016, + 40385, 43460, 98015, 169032, 29546, 317, 16757, 61276, 30502, 211577, 27875, 3900, 22386, 11672, 18862, 7483, + 66527, 116135, 41410, 25526, 107458, 48136, 4627, 31485, 6850, 6851, 18832, 87836, 24022, 18326, 45152, 4289, + 47983, 4584, 50159, 5227, 30603, 9757, 27848, 14186, 35083, 178388, 76847, 56645, 8934, 26884, 969, 107665, + 304066, 106975, 94078, 22863, 39500, 79589, 54285, 52346, 9, 105974, 10324, 22271, 28261, 14018, 68550, 223406, + 33026, 33723, 167920, 84931, 77251, 18488, 61468, 140862, 9374, 16225, 134571, 126469, 44833, 2686, 49718, 124647, + 116312, 1215, 83962, 5572, 34990, 71606, 73680, 62194, 29236, 49106, 112907, 45328, 63563, 57200, 134223, 119112, + 125639, 20182, 135593, 81539, 135405, 182962, 6376, 2944, 165398, 71975, 43868, 92182, 159055, 10997, 72330, 18563, + 55690, 2114, 39931, 35390, 88141, 54250, 18546, 28114, 69643, 110033, 69602, 11752, 236964, 2628, 45862, 81263, + 31983, 28246, 80175, 121337, 25572, 22559, 17702, 19278, 20436, 10543, 47657, 113444, 36746, 10480, 87410, 12082, + 60896, 959, 39265, 183075, 31850, 51426, 10828, 84058, 16179, 1273, 128039, 147289, 11828, 25587, 131393, 39952, + 5888, 121379, 76269, 37043, 3043, 76094, 45039, 146616, 66368, 17415, 378876, 51710, 9750, 1764, 52374, 71144, + 31167, 15312, 56281, 89753, 7915, 15678, 93391, 87084, 53111, 56512, 105225, 38442, 36430, 51574, 77376, 65903, + 1333, 89929, 814, 75533, 64344, 10164, 325602, 55313, 93659, 2487, 16394, 59473, 20061, 3479, 15298, 124327, + 60596, 150371, 102582, 138574, 180191, 71830, 13827, 98326, 51630, 12630, 135, 27533, 6792, 27597, 14192, 33715, + 30244, 143897, 92657, 4323, 43509, 2215, 32766, 14515, 101058, 82207, 7222, 111758, 22409, 5186, 141296, 24798, + 420, 100838, 20522, 137775, 44210, 32883, 95696, 537, 109783, 90107, 20074, 2531, 43223, 14612, 84993, 171076, + 27030, 753014, 54033, 23994, 72477, 11870, 108492, 74863, 72831, 78381, 313, 219089, 32679, 44756, 39365, 53656, + 29235, 112804, 4612, 118248, 7675, 9325, 61217, 115300, 47556, 1974, 176307, 5313, 12258, 24359, 49934, 241815, + 39907, 4311, 18866, 14709, 149412, 89233, 46619, 83155, 84926, 117576, 37441, 44149, 118247, 9581, 245890, 19548, + 24692, 61039, 81797, 23206, 71717, 37350, 97838, 6965, 105038, 174607, 122384, 5456, 86009, 3823, 64576, 41024, + 45941, 19151, 16982, 5901, 41189, 12737, 28662, 9093, 89453, 59974, 36378, 119022, 29856, 38530, 16274, 104168, + 52543, 25096, 44843, 20335, 30627, 25217, 83023, 20190, 226798, 32906, 48839, 122264, 67303, 25118, 78432, 77680, + 59230, 99203, 52453, 97225, 67415, 116979, 48969, 59468, 34408, 89209, 24629, 71301, 1367, 86361, 58029, 35735, + 99685, 35427, 110370, 109975, 16867, 184273, 35211, 185023, 128419, 6443, 43697, 58373, 52147, 56592, 116955, 32140, + 4111, 14363, 213044, 135637, 125381, 38275, 50143, 17997, 117881, 101169, 57994, 9105, 16173, 1311, 63238, 37077, + 44093, 61457, 62582, 8321, 102224, 43737, 126639, 70604, 31575, 13061, 20600, 75637, 23234, 165387, 187842, 19370, + 24870, 41729, 18660, 37654, 83790, 66390, 28158, 66508, 127407, 6641, 102989, 100976, 239098, 111562, 135358, 12923, + 3200, 3321, 50588, 83626, 994, 45525, 35195, 6785, 1255, 50764, 123683, 15659, 100903, 138375, 23653, 10925, + 242275, 55742, 47613, 60776, 117266, 29543, 19014, 83593, 55116, 48171, 71871, 34800, 38205, 1776, 35402, 102985, + 16140, 81921, 23789, 138566, 29662, 15032, 205698, 158837, 11307, 35577, 20517, 75918, 40414, 115830, 81388, 35444, + 208793, 18557, 12624, 88971, 123355, 7306, 709, 13579, 56470, 36205, 56962, 82710, 4862, 23826, 108392, 175539, + 75600, 107919, 62674, 112679, 30119, 36318, 35323, 3583, 58218, 42961, 39675, 9447, 70828, 40967, 25394, 52990, + 24075, 6264, 32106, 43005, 26974, 2988, 36004, 49055, 7802, 84004, 24170, 31281, 192353, 47725, 12122, 11485, + 4851, 11565, 12034, 19433, 5475, 41012, 20120, 169743, 78720, 54606, 9059, 80180, 66596, 18422, 7869, 44705, + 3409, 26996, 13290, 28943, 28573, 92913, 21345, 102017, 64396, 222153, 58203, 28422, 30381, 11224, 105757, 65847, + 30690, 156282, 70213, 52668, 59859, 46136, 55861, 28532, 88256, 167753, 157143, 34619, 5406, 46785, 110077, 43667, + 99945, 9622, 81243, 40902, 103064, 15805, 16903, 64832, 34463, 71025, 48166, 42913, 37727, 31054, 199439, 94651, + 24238, 84843, 164414, 59355, 86643, 74925, 68161, 10544, 60088, 90967, 94387, 626, 4057, 59963, 60891, 3100, + 23741, 51614, 57271, 43352, 5967, 76881, 11240, 21366, 162904, 80651, 76535, 13325, 38240, 4001, 24457, 67064, + 28356, 59452, 277744, 118863, 93858, 59807, 37070, 46501, 25510, 47161, 7751, 47987, 122879, 281048, 12303, 65298, + 6897, 37593, 95760, 68584, 3278, 26040, 46338, 79738, 7057, 17560, 16525, 65646, 11971, 76085, 47810, 37627, + 4400, 76728, 5019, 19862, 35461, 120836, 69334, 34264, 211413, 192855, 77060, 103398, 21395, 73312, 37471, 37865, + 59615, 99094, 60234, 101902, 39471, 232524, 96009, 116049, 87233, 34141, 2404, 47909, 55795, 180204, 127761, 116132, + 128426, 111843, 49823, 18679, 3051, 65130, 52071, 17872, 22470, 42507, 67834, 142462, 41950, 81020, 33544, 172729, + 14705, 32808, 43131, 39318, 3974, 48407, 145523, 8491, 180108, 104130, 40283, 86994, 80476, 22927, 7120, 102446, + 78442, 34442, 85410, 33747, 204996, 201697, 144383, 153362, 91987, 218233, 24213, 28647, 15634, 10985, 37691, 14620, + 67610, 11910, 119218, 53184, 139015, 23331, 10937, 4794, 142373, 40819, 40365, 31288, 35611, 63904, 95756, 235215, + 51134, 76276, 11957, 30265, 10387, 46958, 85389, 177045, 4353, 39988, 117622, 35975, 153456, 14265, 42420, 3485, + 57749, 23217, 14856, 159478, 181039, 125928, 75417, 128987, 14183, 7005, 18444, 37640, 68447, 15249, 32753, 189408, + 151532, 34992, 102201, 115188, 21107, 39324, 56039, 38368, 36452, 94812, 113679, 87546, 20551, 11761, 60872, 2888, + 3186, 69267, 53191, 52996, 46247, 44225, 33896, 98339, 46383, 32566, 16275, 88963, 129666, 176, 31427, 38271, + 88736, 11275, 58147, 11767, 140662, 14422, 96597, 45066, 146243, 18516, 134527, 23160, 2066, 107751, 37546, 10292, + 8360, 95441, 58117, 98151, 7978, 116856, 906, 79063, 64818, 14019, 109894, 9029, 106963, 34922, 1651, 42838, + 17896, 207329, 33682, 4066, 47801, 29346, 56026, 102181, 10723, 23963, 12845, 30084, 114821, 24728, 27351, 96712, + 223295, 24805, 8840, 155257, 74192, 14731, 60882, 14446, 3293, 36369, 62442, 16908, 3393, 24144, 93518, 169099, + 16987, 21052, 64385, 3253, 74064, 34395, 37448, 95011, 11277, 73486, 191440, 28196, 91622, 56473, 327982, 27442, + 4270, 7603, 34045, 68072, 29828, 36418, 35213, 46824, 27951, 92979, 6344, 294949, 387869, 35280, 6512, 67496, + 103235, 38638, 120453, 15900, 1374, 3046, 370, 32475, 61988, 25666, 311175, 38789, 120083, 14581, 62496, 17476, + 477, 95410, 36028, 79370, 145892, 21060, 64274, 8593, 128378, 29734, 47799, 41023, 11779, 189177, 291116, 68655, + 211263, 3096, 34673, 28182, 61354, 70851, 185841, 5926, 18221, 58964, 5624, 148475, 17869, 70577, 270, 72725, + 46530, 32794, 6345, 53885, 83061, 319994, 77613, 55607, 108538, 2964, 83165, 113358, 157981, 31861, 142777, 32134, + 90608, 77733, 12681, 67299, 67199, 12519, 151204, 80716, 95080, 143, 88673, 73243, 49064, 70975, 92554, 194270, + 195814, 37976, 26210, 15538, 12302, 590, 123979, 34036, 66307, 32917, 51270, 50898, 10348, 192749, 78998, 142567, + 231346, 74689, 141501, 68754, 160732, 81034, 15915, 13236, 112859, 19495, 33767, 7063, 63633, 6869, 25702, 6584, + 146558, 146882, 202224, 116085, 21271, 11451, 46600, 73942, 31037, 225039, 32805, 68328, 198802, 8912, 109275, 23107, + 47622, 21867, 446, 51547, 12862, 75989, 33036, 7283, 95710, 83109, 38497, 1301, 3910, 40681, 44384, 46771, + 77850, 49203, 95089, 139563, 73961, 6370, 84201, 1459, 85585, 161489, 169649, 79787, 34752, 252385, 3378, 55032, + 61000, 3120, 4672, 121269, 4082, 53803, 18080, 73246, 24595, 42389, 13731, 72203, 103679, 101774, 38710, 34206, + 71107, 64573, 16007, 64268, 8208, 29590, 28108, 9501, 79568, 11666, 168288, 83632, 150019, 35196, 5403, 5176, + 16615, 93441, 6104, 17287, 24961, 112281, 14480, 245496, 139857, 9685, 75837, 34655, 32664, 20702, 201412, 31683, + 197366, 202353, 26591, 31589, 4559, 27910, 10695, 29436, 54735, 144360, 113352, 52336, 32696, 146974, 38124, 34893, + 4126, 3855, 115855, 6057, 162019, 10972, 91491, 48259, 75698, 34626, 7354, 16587, 13916, 6749, 69092, 102728, + 70108, 121378, 4399, 25253, 159638, 105250, 11905, 98411, 19834, 40029, 8256, 251243, 9349, 57694, 5558, 143250, + 24675, 51457, 65260, 24058, 175560, 27907, 52475, 49070, 49643, 4320, 66836, 90101, 18206, 8928, 62901, 87851, + 52459, 14454, 36601, 64627, 27992, 417, 30565, 19686, 10809, 231322, 36540, 40887, 88865, 49589, 161033, 58107, + 401975, 102386, 190339, 87577, 133172, 54023, 98944, 20896, 29000, 84825, 33172, 11781, 34558, 116897, 58352, 53344, + 30915, 21521, 89, 241320, 3658, 45946, 115393, 212186, 25834, 50333, 3228, 37254, 42430, 1886, 49132, 81794, + 36562, 28818, 44722, 31564, 125265, 149984, 17397, 128685, 18182, 3056, 21825, 25140, 10155, 16556, 115060, 11990, + 40149, 184644, 50253, 367370, 97082, 13560, 14932, 13300, 208980, 36379, 448, 106133, 19575, 21267, 68757, 93452, + 60853, 5169, 34427, 16161, 90529, 142432, 73614, 192405, 66545, 49751, 90066, 65857, 9600, 4098, 80224, 55076, + 789, 3798, 125169, 73968, 46420, 59540, 51655, 28547, 2317, 95529, 72038, 360298, 88593, 260668, 151609, 120315, + 55595, 35600, 10778, 130706, 98980, 30010, 17825, 47077, 115302, 28222, 240315, 7342, 5742, 15051, 17234, 17561, + 169155, 93735, 18784, 51808, 1073, 87638, 22486, 96835, 177901, 34933, 43751, 134471, 3472, 60107, 8319, 10366, + 11189, 23719, 97590, 116678, 63711, 15095, 47410, 210265, 78643, 127925, 2850, 146459, 65472, 435141, 43043, 51101, + 50459, 17859, 2597, 1029, 127979, 110999, 140586, 131287, 93, 204861, 49176, 53532, 42202, 49335, 38681, 33970, + 67053, 39163, 64804, 7867, 21720, 57269, 35446, 82892, 157650, 75789, 40884, 15694, 11145, 39407, 108823, 27310, + 141378, 15460, 227, 20019, 42033, 6822, 24630, 75665, 22824, 20941, 64884, 46365, 85705, 3773, 32852, 75547, + 79114, 70646, 91197, 26198, 35584, 16783, 11785, 51303, 15974, 155610, 115243, 9935, 1510, 56327, 108654, 387235, + 54172, 2660, 22731, 69831, 28562, 32500, 45536, 94042, 12451, 40584, 20876, 3006, 104226, 49255, 2735, 68192, + 19190, 61831, 13307, 161983, 97151, 164345, 4367, 129297, 73024, 45668, 60180, 126542, 20948, 27490, 11817, 6231, + 5151, 13473, 7833, 109917, 81741, 27736, 71450, 26743, 21499, 244239, 47508, 47275, 29006, 25257, 7357, 18329, + 84183, 112580, 118605, 3793, 198074, 159004, 32283, 17771, 54003, 17983, 52972, 70724, 45120, 24182, 47404, 99628, + 170125, 8794, 46876, 41664, 26240, 122522, 32698, 169875, 35177, 43594, 40030, 96952, 28389, 22133, 1219, 1627, + 64863, 236968, 142742, 94455, 79974, 94146, 13684, 19300, 60778, 15176, 21046, 112121, 176915, 177946, 121876, 170733, + 232183, 45571, 52925, 2209, 45342, 34927, 19843, 1825, 2038, 21606, 30609, 39291, 80253, 114805, 22110, 67637, + 41564, 11345, 145262, 126398, 40703, 91578, 60920, 14383, 32689, 114171, 33321, 22821, 5430, 118359, 18515, 41276, + 100689, 31373, 44534, 57652, 5366, 38077, 63639, 246112, 23007, 50484, 65271, 114639, 134279, 23472, 42037, 36268, + 14266, 3805, 20110, 106077, 26712, 83068, 3745, 48789, 73993, 11755, 10138, 6567, 24934, 1686, 24404, 64978, + 64242, 102615, 49674, 64915, 52113, 102967, 47100, 123729, 102887, 34493, 5532, 124741, 61801, 68500, 69254, 101322, + 46415, 189970, 103910, 112087, 201049, 82512, 10888, 99736, 54251, 19508, 119320, 20798, 62133, 100781, 96032, 438249, + 122757, 112255, 29169, 17982, 164883, 72196, 54239, 5731, 30815, 71455, 40605, 9088, 139966, 231725, 23187, 27251, + 2319, 15295, 22292, 9157, 30842, 13822, 93710, 35112, 766, 17075, 23218, 37794, 13362, 14364, 86554, 81170, + 10287, 35041, 29695, 20186, 134518, 87266, 22035, 118809, 86111, 54826, 39227, 34957, 81665, 785, 130193, 5422, + 82440, 165380, 20305, 170800, 28333, 3085, 25031, 63282, 43019, 46076, 6077, 19050, 18963, 12418, 78312, 37302, + 8804, 89631, 234705, 12675, 161944, 14463, 408482, 32815, 23439, 151647, 193595, 35885, 102144, 76974, 201816, 48443, + 101145, 61088, 67380, 55214, 80029, 65112, 236169, 11787, 39052, 61274, 38785, 128619, 248708, 117197, 271163, 7859, + 30350, 146035, 2253, 131342, 117340, 101888, 58102, 64465, 11878, 89316, 34052, 26133, 128467, 54644, 138482, 21706, + 974, 21544, 138848, 29501, 138625, 13861, 594984, 69791, 63961, 7884, 49187, 75381, 5237, 3488, 22429, 62564, + 74778, 10035, 87433, 84117, 61834, 89252, 99881, 82523, 67040, 20877, 122047, 11631, 43814, 146638, 2274, 23298, + 13690, 16936, 14388, 11469, 65947, 133203, 65149, 34934, 33809, 57431, 38649, 14721, 232476, 110369, 46966, 79425, + 115258, 73854, 38503, 168461, 181745, 103268, 1590, 73761, 28824, 23871, 104328, 10944, 94013, 11048, 79005, 77074, + 9510, 89479, 89361, 8471, 10246, 73385, 17529, 51173, 93722, 40211, 452, 112593, 81976, 33181, 50233, 31287, + 7217, 11605, 5815, 49818, 114383, 88600, 20714, 201753, 3493, 31374, 99143, 103003, 16014, 10855, 96263, 3215, + 69045, 19108, 17235, 118289, 72692, 54171, 110308, 6337, 12145, 34602, 29627, 82100, 80981, 1942, 94550, 34461, + 9507, 33064, 52111, 30229, 6692, 117458, 68421, 89525, 1620, 80775, 26613, 26281, 60820, 8784, 48514, 22303, + 330444, 101568, 44180, 117514, 35474, 54648, 52855, 24987, 33962, 73393, 65642, 67569, 4797, 67532, 33609, 31625, + 7053, 54521, 80888, 3839, 295463, 120005, 12078, 28541, 46445, 187065, 33145, 165188, 27026, 38584, 77641, 63748, + 12491, 8511, 283918, 4716, 77988, 19152, 55724, 61518, 49524, 87047, 241122, 13621, 35675, 5771, 49017, 17443, + 90947, 23528, 8917, 92592, 29114, 23367, 38345, 16221, 166705, 19640, 54603, 106502, 101385, 34100, 50779, 50417, + 133782, 46043, 51856, 22308, 32704, 72281, 6041, 33229, 6186, 116558, 164648, 67105, 84595, 74801, 41853, 10043, + 176031, 39148, 41713, 36877, 185623, 77660, 93112, 18322, 45966, 122806, 108388, 45184, 151302, 14234, 62763, 55556, + 63069, 244438, 9876, 276168, 1699, 17487, 9500, 35260, 107491, 149204, 1781, 17579, 947, 100336, 84387, 8106, + 15458, 23215, 396223, 29684, 74452, 123641, 92338, 31473, 196212, 62391, 1527, 3879, 6046, 20893, 200851, 3778, + 10498, 7764, 38750, 23256, 12163, 76975, 57473, 59530, 10239, 23275, 35839, 158531, 35191, 44209, 40678, 3596, + 243951, 73764, 16266, 15283, 9277, 164, 9335, 59492, 9090, 7474, 66893, 9054, 29539, 12905, 6948, 246436, + 54460, 50334, 52706, 46765, 22820, 125379, 163547, 191059, 26514, 8972, 72494, 117307, 112549, 40233, 12782, 33432, + 60372, 35994, 32356, 367449, 51753, 39462, 62458, 34885, 48756, 795, 117374, 148644, 21812, 14674, 1704, 10995, + 70861, 98992, 38556, 29671, 260326, 35167, 3569, 19709, 41, 10312, 47172, 133362, 44222, 19304, 3828, 39543, + 10441, 32694, 2925, 24827, 16961, 14477, 106226, 11249, 48148, 641, 182648, 47579, 138771, 11517, 1834, 5495, + 216194, 89574, 56707, 54758, 5914, 36178, 72805, 5272, 52153, 156729, 2240, 13954, 53400, 68748, 63310, 189742, + 212036, 48502, 106813, 55645, 56519, 64696, 48413, 131579, 26245, 26436, 62198, 36864, 10117, 183822, 41455, 133034, + 45888, 134173, 151941, 23987, 15294, 9465, 151116, 213962, 138019, 23156, 20652, 128889, 90913, 19364, 219476, 43092, + 26368, 135383, 5308, 63585, 43842, 38292, 83051, 52878, 42111, 91099, 61824, 86809, 23348, 16103, 1661, 1463, + 6082, 2926, 10571, 10834, 194845, 212415, 28967, 55622, 161089, 61297, 359805, 83283, 156206, 39861, 2052, 124838, + 51546, 15485, 2858, 116911, 11647, 165218, 246102, 28861, 30759, 90050, 115426, 107685, 302912, 221668, 43284, 28078, + 262094, 15913, 108493, 34355, 8635, 56521, 33938, 107963, 78876, 130940, 120485, 7453, 26535, 294502, 79190, 217099, + 35283, 16638, 827, 171026, 54183, 254986, 24436, 15975, 31183, 71531, 8757, 197433, 85484, 10671, 7791, 98566, + 147873, 81926, 428971, 27061, 12989, 34063, 127862, 4570, 5197, 115338, 86529, 11135, 6356, 99904, 45008, 275758, + 72894, 23300, 64365, 137852, 65347, 47170, 28144, 96791, 20150, 159397, 90399, 1362, 27370, 98131, 31669, 28527, + 73787, 117782, 21103, 19726, 1493, 47288, 66521, 29621, 45918, 29807, 100648, 6707, 12366, 36485, 45413, 16410, + 190217, 44950, 2222, 75028, 20724, 41617, 33388, 41062, 13858, 83447, 87017, 3916, 10981, 5912, 19979, 5885, + 67449, 21903, 300914, 5663, 81213, 18877, 80072, 19338, 7553, 143149, 63928, 73907, 14115, 297533, 1424, 109439, + 72242, 21063, 30733, 14100, 271517, 34775, 24860, 63700, 11842, 20737, 50067, 22773, 48310, 11235, 43957, 13183, + 88743, 32209, 23321, 97092, 143726, 17704, 33289, 144394, 22177, 68374, 111967, 982, 3290, 170312, 8945, 39607, + 243231, 56247, 9319, 54873, 58452, 181674, 44954, 10669, 62937, 133402, 31264, 14854, 12592, 70761, 4902, 2472, + 1654, 39097, 17353, 115270, 40066, 92333, 7620, 158039, 33477, 80720, 2135, 38418, 13751, 164773, 39322, 114253, + 9921, 17904, 79739, 5389, 128442, 4659, 46992, 27136, 15868, 109620, 82964, 2344, 7106, 102119, 3920, 13567, + 75236, 23651, 9863, 2844, 83773, 92000, 12422, 95077, 10775, 142605, 120027, 9048, 36938, 81154, 56545, 4754, + 10482, 28907, 16084, 21873, 170465, 19577, 24905, 58130, 17368, 70780, 14531, 55034, 17469, 23293, 260571, 52064, + 161508, 63795, 195965, 205503, 32752, 145584, 138232, 298398, 98340, 15352, 275003, 75638, 800, 306996, 12394, 36516, + 19824, 101316, 192935, 84781, 264456, 14840, 73856, 120271, 3901, 50990, 48385, 372041, 87319, 2227, 95681, 56225, + 2867, 24632, 65645, 172829, 26782, 83816, 46844, 37867, 9630, 11329, 76756, 138080, 113102, 2296, 84207, 55323, + 185815, 5210, 2986, 109112, 24197, 70199, 64611, 7066, 44584, 96679, 3292, 11955, 86366, 7796, 105090, 121463, + 40224, 45210, 50967, 43523, 3636, 166133, 21094, 57178, 140916, 4748, 13764, 3777, 31731, 104179, 416810, 188439, + 267731, 86931, 13966, 5018, 9567, 283051, 3232, 53171, 53678, 6228, 32833, 99759, 72984, 37061, 70162, 70738, + 29389, 1868, 122000, 13671, 27963, 37380, 69277, 37341, 17106, 45005, 12564, 183, 50282, 3809, 57468, 23868, + 284911, 75942, 28036, 2447, 60170, 8119, 51479, 25171, 8322, 48880, 42721, 33419, 12608, 11361, 18423, 24958, + 23374, 141776, 2304, 149128, 89652, 68789, 36035, 45613, 5268, 442, 159096, 63783, 39044, 4432, 34374, 26718, + 229766, 59526, 23706, 15072, 8869, 12775, 73562, 76513, 151350, 46040, 29341, 58942, 31436, 8742, 195663, 155875, + 177342, 9409, 11232, 106379, 12269, 40493, 135684, 74614, 183212, 41598, 44475, 43186, 120418, 263054, 22473, 36531, + 116270, 43205, 64216, 41880, 2843, 3299, 84033, 48575, 78888, 37197, 53158, 44555, 69192, 29213, 80495, 156103, + 7865, 86885, 75088, 21642, 184099, 132386, 508, 948, 1086, 69319, 44488, 3613, 129897, 33311, 51024, 2449, + 18383, 28970, 50210, 70726, 70508, 87097, 118602, 146382, 20242, 182430, 197233, 39937, 18508, 14350, 24901, 165385, + 229924, 33832, 136628, 151243, 124569, 88492, 127154, 62624, 35749, 21909, 91751, 8060, 50589, 954, 105732, 25763, + 55626, 146107, 1247, 64529, 9884, 13632, 16172, 1335, 83115, 126164, 36224, 170872, 40971, 2035, 7394, 46551, + 30671, 53142, 107311, 75486, 18135, 69492, 12903, 96202, 14452, 18152, 49690, 77016, 38861, 2226, 385130, 21131, + 17844, 57784, 84, 36610, 201826, 14842, 131564, 77270, 5549, 102741, 193961, 183742, 26413, 2474, 194304, 464074, + 17189, 226650, 166968, 718, 13561, 32539, 7182, 35594, 38539, 16307, 188634, 18219, 10679, 72826, 161909, 63158, + 143331, 23378, 14208, 25491, 3314, 73171, 63331, 5500, 36785, 38973, 614, 1580, 171194, 97209, 137201, 91854, + 49685, 50187, 10733, 91809, 187713, 62737, 172516, 6666, 67506, 4776, 58141, 30188, 4618, 392, 4013, 235640, + 104039, 39136, 56441, 250251, 17060, 48306, 57483, 66360, 195080, 59703, 90208, 74747, 50648, 37806, 27833, 30715, + 33159, 66182, 9356, 34911, 19238, 70986, 114677, 17409, 67559, 80594, 47694, 8643, 134840, 79148, 5452, 14070, + 28599, 144163, 50434, 27535, 157523, 13810, 155, 61478, 17130, 128237, 70556, 20046, 38064, 108184, 48322, 65305, + 117398, 36136, 43677, 27966, 94355, 126236, 109335, 98906, 31918, 54262, 1145, 25681, 13575, 60774, 45912, 122417, + 34538, 24034, 131365, 51834, 40326, 21250, 21866, 17508, 13997, 117733, 16819, 89310, 3494, 11178, 60427, 34853, + 348283, 6846, 88202, 17221, 62481, 8359, 10088, 62152, 26862, 12931, 13414, 151495, 3603, 33643, 178131, 46655, + 104426, 109103, 4105, 146806, 244363, 64272, 15471, 93101, 153709, 21364, 73754, 69185, 112487, 8238, 52934, 80498, + 304612, 124178, 39721, 198761, 199674, 132729, 36479, 5833, 41239, 3609, 28387, 262648, 35545, 67430, 89355, 18851, + 54869, 54495, 5118, 27316, 293005, 36308, 43061, 110038, 28223, 7186, 33116, 18945, 26277, 73156, 23311, 41090, + 26899, 300052, 100683, 8894, 4533, 2915, 66118, 31965, 18518, 75755, 209592, 62868, 15492, 99745, 54220, 111614, + 38587, 26556, 107426, 95125, 80488, 64232, 13265, 23009, 70485, 25431, 69000, 3572, 160395, 98028, 42484, 42093, + 263, 6747, 244525, 132822, 60162, 171913, 124805, 29698, 11382, 8416, 48535, 37248, 222152, 16593, 47020, 8816, + 4696, 24033, 36100, 38399, 250751, 10483, 19137, 143486, 51921, 74917, 130735, 79897, 182609, 69122, 71667, 39358, + 10707, 133977, 56093, 112728, 48463, 33430, 9299, 48097, 46243, 217556, 63573, 36225, 1227, 221850, 32274, 14107, + 49111, 29646, 36094, 63601, 111564, 49362, 24638, 17610, 46502, 41875, 40909, 4262, 33342, 32777, 32103, 28673, + 56846, 6159, 154445, 47336, 68541, 153151, 3385, 58607, 63559, 108092, 46159, 13477, 858, 2316, 24161, 128413, + 139927, 11071, 88393, 110949, 16654, 34235, 73235, 37210, 229375, 76831, 194659, 71471, 76759, 4725, 28276, 29321, + 26478, 117042, 46296, 101434, 33205, 67157, 6462, 3168, 95828, 54864, 23765, 55657, 23399, 39175, 47265, 10732, + 92945, 59744, 14719, 35115, 2637, 23170, 162802, 33876, 35630, 38052, 9197, 83972, 28470, 19030, 22779, 259343, + 143992, 229198, 23683, 129194, 50214, 30041, 34367, 134697, 14174, 184114, 144727, 23833, 21456, 25715, 53623, 10461, + 166191, 57432, 28691, 157504, 65665, 67457, 16937, 12529, 1711, 168178, 4710, 9397, 21594, 15069, 102984, 295316, + 78019, 30013, 49809, 108210, 97599, 11539, 20109, 22376, 111701, 47783, 890, 213373, 36, 229081, 150468, 50773, + 147151, 857, 99218, 78079, 110246, 83001, 17917, 78802, 189022, 112785, 40738, 2303, 43021, 10227, 101710, 85107, + 30397, 3429, 72517, 49710, 40757, 160978, 8573, 100346, 131935, 103276, 38380, 105759, 42065, 28883, 402, 95838, + 73335, 67055, 20011, 29906, 48039, 54843, 18117, 35768, 26596, 8518, 143614, 74994, 28984, 30865, 51327, 59362, + 15102, 44276, 89118, 714, 2361, 214493, 160772, 63295, 7421, 71563, 196497, 21076, 202167, 92741, 103226, 106818, + 69744, 89977, 100205, 98932, 43766, 76931, 6843, 16584, 52826, 291470, 15946, 28322, 3642, 204, 258863, 106515, + 83304, 32946, 12706, 41427, 33873, 27151, 56397, 63503, 75140, 34306, 73149, 29926, 63169, 91359, 77492, 74847, + 192389, 42353, 148, 138944, 36551, 31192, 181456, 63005, 92748, 24635, 51573, 52932, 13039, 4741, 79294, 69836, + 123959, 107948, 11123, 170654, 233220, 54352, 20575, 40997, 21738, 120351, 106134, 155424, 84447, 124076, 35541, 706, + 77230, 51748, 8442, 40109, 20228, 6042, 20963, 107207, 187852, 127504, 15461, 33093, 19095, 5987, 21488, 118624, + 25799, 182308, 23326, 20103, 92136, 54838, 10713, 126470, 108774, 19203, 61066, 63342, 29237, 9113, 45526, 43035, + 53947, 312166, 11117, 25827, 2299, 27218, 55737, 42929, 118106, 61182, 16888, 118952, 2687, 159550, 3496, 59443, + 8830, 51688, 78340, 103766, 42331, 73399, 58020, 1666, 202924, 2854, 12678, 5891, 33667, 17, 36329, 9733, + 2023, 40389, 124099, 86266, 73763, 17618, 9348, 9584, 30704, 89527, 11379, 88708, 19363, 71305, 80907, 163488, + 19779, 4406, 18155, 125871, 16737, 4313, 24007, 71010, 35629, 59187, 181773, 171755, 48081, 282188, 44159, 41703, + 24068, 63512, 20026, 20079, 101013, 4611, 19597, 169832, 162338, 158889, 21816, 59081, 291912, 51657, 16911, 142760, + 13749, 22555, 50557, 197345, 24745, 28474, 5713, 171571, 328289, 6161, 7725, 36994, 167679, 62776, 62795, 52274, + 70086, 34392, 130952, 4930, 48299, 107549, 36520, 19777, 23306, 51480, 107581, 7841, 16732, 37422, 72074, 99464, + 17801, 125916, 10597, 40408, 43322, 87432, 24862, 37097, 54589, 32282, 167348, 17898, 3586, 19826, 146462, 1988, + 63653, 28763, 132195, 94493, 43624, 52107, 37692, 42666, 53474, 25957, 8805, 222451, 925, 3055, 104713, 68687, + 109177, 128137, 51766, 12112, 251316, 83233, 11015, 24963, 43805, 189880, 86830, 77075, 13082, 33163, 73548, 44568, + 19511, 108517, 64550, 26047, 86565, 54739, 5095, 68246, 142182, 40984, 140026, 1750, 92461, 2773, 725, 19988, + 17117, 106417, 21066, 147935, 101033, 174174, 12814, 99891, 103319, 85050, 87912, 55182, 64589, 75793, 37143, 22056, + 4022, 81127, 18968, 52899, 4351, 50009, 17573, 31885, 235897, 86925, 127636, 149633, 5352, 170945, 13870, 65479, + 82705, 21354, 52980, 4573, 107142, 167201, 689, 25288, 46391, 60641, 14497, 192600, 156084, 61637, 16433, 20054, + 5860, 781, 55170, 27826, 61365, 81069, 119317, 27123, 10558, 95773, 129802, 55875, 13045, 36330, 234176, 18058, + 7717, 148000, 254, 11979, 18357, 24975, 8185, 10718, 33922, 75655, 63067, 62116, 12590, 26192, 24136, 12861, + 33065, 175633, 5001, 83234, 6928, 37537, 39570, 797, 46993, 106751, 3390, 24316, 783, 56130, 98315, 10024, + 46937, 84478, 81419, 119922, 67846, 100404, 45582, 91490, 8952, 68431, 40501, 170957, 26295, 27641, 117433, 78412, + 6107, 100280, 74320, 156460, 119656, 6001, 172558, 13585, 18799, 5324, 39108, 12471, 17458, 37351, 39676, 88115, + 50747, 61250, 125687, 5267, 4229, 23899, 71256, 32369, 179559, 203977, 81060, 24631, 112727, 201216, 111416, 67474, + 118080, 91, 35301, 125372, 20683, 74602, 93673, 189634, 41464, 103487, 832, 23478, 125468, 73805, 119649, 29439, + 51560, 85326, 160218, 36794, 49749, 182459, 10783, 266813, 44231, 9294, 15428, 71516, 7359, 56127, 165213, 174634, + 35339, 38024, 193163, 64310, 62988, 135393, 50470, 543, 136487, 82389, 59047, 12586, 67015, 11644, 14120, 11086, + 5208, 84496, 75693, 97070, 29150, 13909, 133818, 28826, 24956, 25564, 136586, 132437, 105186, 4035, 36426, 22688, + 48858, 283040, 60007, 145362, 6143, 70935, 12860, 88632, 18097, 251246, 43147, 88692, 6972, 46173, 87229, 69837, + 16404, 61816, 34813, 41161, 73489, 67221, 80668, 80317, 58742, 40484, 31106, 50580, 97196, 213949, 58374, 30823, + 36357, 82430, 16088, 18269, 164616, 424, 215366, 24797, 5834, 261900, 3248, 32765, 32267, 19570, 240015, 43546, + 13746, 95242, 296368, 118754, 147733, 19239, 38636, 84832, 15113, 3902, 193061, 119038, 132091, 99171, 11021, 33759, + 34127, 5030, 3999, 6802, 106298, 88781, 43305, 22438, 39729, 5081, 25109, 217719, 106426, 21384, 30890, 42599, + 22294, 25962, 25245, 30497, 9780, 59534, 29560, 5927, 15602, 52507, 34738, 56980, 36213, 7273, 98748, 10870, + 71502, 69490, 359492, 120808, 42808, 83335, 17061, 250432, 66802, 102331, 12384, 25660, 599, 64618, 47661, 61634, + 60755, 54355, 107300, 30007, 5851, 79666, 18845, 114230, 39120, 65244, 33679, 16370, 67363, 870, 69190, 700, + 108623, 7199, 25596, 17155, 126368, 82661, 16291, 21557, 72770, 55038, 123010, 8616, 91263, 105277, 4143, 14045, + 32486, 45280, 15555, 228392, 30596, 287, 109234, 90030, 151717, 60950, 12314, 165069, 7951, 13304, 63878, 123573, + 52002, 27428, 9709, 27443, 43103, 89429, 81209, 45635, 11768, 65127, 12093, 62760, 68942, 4189, 59598, 70512, + 40901, 34082, 60981, 18280, 39344, 77285, 4183, 14054, 24037, 4113, 474190, 227569, 127500, 33437, 1737, 48639, + 116890, 82909, 62991, 47953, 48403, 63792, 44541, 194078, 16926, 13540, 54497, 288031, 86750, 30818, 25377, 51309, + 17745, 9988, 98587, 47808, 48648, 15607, 51298, 11671, 159545, 43990, 97815, 27393, 34460, 59095, 50213, 40288, + 58419, 77017, 108711, 106974, 5634, 27302, 23195, 55246, 114317, 20787, 2176, 34170, 67865, 144530, 7099, 31816, + 31462, 136214, 45619, 105094, 23352, 133565, 14615, 72015, 24010, 107270, 1347, 151444, 98185, 20971, 63491, 86427, + 125708, 141576, 126803, 83272, 69686, 130286, 27346, 20481, 68337, 143641, 11161, 32803, 13610, 94361, 17084, 170992, + 26271, 23139, 77797, 5076, 70691, 1893, 34857, 35003, 2980, 189255, 41107, 6458, 4768, 19194, 182508, 92031, + 27225, 316351, 48574, 15987, 102402, 14334, 46503, 138005, 75453, 29119, 31677, 97327, 28106, 72279, 7983, 6958, + 8104, 6430, 66373, 51695, 6931, 86975, 52340, 108203, 1176, 75897, 106677, 21721, 6274, 7082, 13018, 139466, + 6475, 73402, 49676, 46350, 112635, 1121, 31018, 39288, 22498, 157722, 11740, 4237, 6176, 145143, 8139, 77918, + 238686, 12912, 106531, 13530, 26832, 8235, 79497, 23768, 28893, 17853, 435790, 33701, 90319, 24688, 121374, 13130, + 14441, 44878, 2951, 60805, 15682, 368968, 59313, 5764, 15087, 226921, 19420, 46906, 39517, 28151, 141168, 18755, + 45270, 39523, 7813, 6413, 109134, 51672, 66939, 115004, 104440, 22565, 91467, 22538, 45965, 62165, 14231, 46295, + 47645, 71747, 30576, 43847, 81772, 99248, 106345, 17063, 7876, 115604, 42345, 60383, 52683, 130873, 7327, 83603, + 87720, 43843, 117, 49018, 12898, 24681, 9075, 145197, 4505, 69956, 86690, 8844, 185665, 73412, 8843, 2050, + 2769, 61489, 84788, 765, 113401, 48729, 111522, 169599, 15664, 24750, 131177, 29882, 57592, 149057, 45527, 23022, + 105229, 1754, 10064, 14643, 137381, 30538, 7740, 43774, 97059, 30153, 99882, 62539, 119268, 49329, 86015, 5395, + 6876, 63855, 14385, 103785, 43309, 58548, 24380, 31077, 33886, 37135, 15365, 50313, 128363, 48659, 56802, 25981, + 35476, 24927, 75162, 26509, 144249, 18559, 42480, 122475, 67013, 7834, 35825, 193197, 143587, 15263, 5498, 114311, + 83367, 56008, 102437, 38184, 25703, 112278, 26497, 8942, 91436, 71042, 111589, 153704, 59347, 5258, 57333, 17950, + 53236, 28574, 30025, 337749, 2289, 1610, 194412, 103935, 16519, 41580, 29330, 37510, 19844, 35382, 26043, 158342, + 46309, 11301, 106938, 96214, 58558, 67686, 163604, 7639, 99834, 7952, 12195, 58864, 23313, 15895, 25042, 10540, + 218816, 17583, 10486, 66978, 231303, 9956, 202649, 2197, 36388, 7432, 55426, 2224, 51333, 16871, 119452, 179548, + 183535, 153812, 138057, 9695, 109792, 28563, 126806, 52451, 139277, 13092, 48380, 9516, 54306, 67549, 14696, 110080, + 90139, 139123, 3236, 10377, 18235, 70961, 50653, 11129, 8275, 44976, 10027, 18291, 32710, 28012, 318969, 288958, + 37677, 67929, 68821, 98990, 82464, 10181, 44498, 70197, 86025, 8013, 28549, 83043, 92204, 6171, 76895, 4556, + 88842, 98320, 60377, 71627, 117723, 32809, 5961, 12916, 37570, 27325, 108946, 2654, 128723, 67148, 211765, 80004, + 234242, 154947, 40300, 196564, 76350, 3941, 18870, 24018, 73795, 84945, 197515, 8338, 34896, 58219, 146191, 58500, + 148247, 85921, 101683, 117085, 58424, 66764, 17133, 63784, 11105, 84682, 86491, 32061, 11744, 40778, 32197, 195238, + 45746, 2576, 39893, 67918, 63372, 3938, 36907, 4069, 17118, 24568, 96382, 59551, 49772, 244074, 72416, 53854, + 199520, 69813, 60862, 51724, 81902, 122032, 234459, 29839, 38004, 36647, 114474, 13978, 22911, 158690, 15088, 63654, + 33752, 29885, 83141, 89560, 3125, 12877, 21039, 57564, 1995, 38775, 1392, 70857, 53792, 84731, 5519, 86340, + 4689, 95886, 119040, 180041, 26909, 84982, 2582, 213702, 108150, 123127, 35785, 15401, 146062, 49004, 67250, 232281, + 69674, 142589, 97906, 160196, 41811, 23716, 17807, 35367, 161444, 15020, 30993, 67673, 84855, 49859, 39473, 23557, + 8999, 127662, 19183, 120775, 28561, 79489, 57003, 62500, 16731, 29948, 30509, 54712, 93937, 166789, 11042, 61414, + 3189, 128681, 363904, 9363, 21967, 135864, 94929, 23174, 24890, 236852, 51310, 35602, 22943, 58275, 6351, 33135, + 1356, 27798, 41855, 110968, 145300, 139274, 56821, 8658, 51569, 19894, 24832, 4253, 28802, 5732, 52333, 338701, + 517, 144012, 123400, 70750, 118679, 112674, 109716, 66301, 31703, 84657, 45777, 1745, 40607, 17239, 226055, 50256, + 48098, 24528, 28411, 109729, 108854, 16675, 111456, 14807, 25003, 27471, 42491, 22378, 10233, 14158, 70447, 13850, + 73969, 15024, 24742, 25518, 177495, 27226, 176504, 38550, 5248, 41612, 65904, 91342, 24516, 41883, 18419, 84650, + 215347, 15434, 75579, 9614, 146192, 82954, 25501, 30483, 48712, 34315, 70905, 29488, 60626, 66089, 51329, 5601, + 69188, 18936, 21518, 23440, 40735, 224481, 33618, 40631, 5866, 927, 128437, 30586, 586, 52791, 76586, 141284, + 101541, 81564, 12333, 65243, 6509, 6267, 176039, 133405, 47590, 16079, 254143, 27357, 52129, 34758, 9267, 15970, + 5969, 57732, 7254, 86956, 222045, 17428, 16267, 26799, 110933, 58017, 142888, 143524, 25733, 55763, 16175, 9560, + 24223, 247240, 55864, 48197, 65339, 12856, 21320, 46799, 62812, 40007, 188763, 14523, 2414, 31539, 49494, 58075, + 155418, 90186, 99708, 24554, 35819, 75001, 757, 39520, 16022, 59445, 3713, 46416, 78423, 112394, 18048, 40416, + 43138, 69398, 67029, 137948, 20995, 20115, 42968, 1859, 128255, 8554, 13664, 13508, 240673, 36331, 63579, 21029, + 46745, 152929, 19469, 227297, 236093, 41575, 99905, 13097, 72176, 7570, 11923, 37300, 57085, 104327, 59863, 51790, + 97841, 7674, 89187, 121357, 61248, 15021, 6833, 112841, 107, 73638, 39990, 166290, 36068, 78500, 50124, 104807, + 193177, 49274, 90762, 21097, 105427, 14711, 17114, 26796, 55726, 20684, 123636, 20366, 215229, 140553, 26789, 7139, + 20446, 136219, 5009, 2402, 47228, 5882, 154075, 61745, 100420, 43241, 49334, 149951, 87091, 86136, 329294, 49115, + 14429, 27425, 36818, 111800, 121708, 18542, 6570, 27157, 23605, 12467, 6000, 70237, 21157, 88964, 42071, 18521, + 187721, 24444, 86, 29324, 21880, 145192, 303927, 17373, 2997, 108310, 171873, 60425, 203976, 18220, 116526, 69373, + 99166, 6387, 84942, 187663, 95068, 21687, 241311, 45047, 25877, 42751, 35432, 9804, 7724, 82982, 23192, 146653, + 98925, 132997, 92522, 87415, 83401, 53505, 150646, 8142, 4829, 58649, 25534, 26193, 13182, 71621, 73803, 70797, + 18229, 39127, 69038, 57676, 13718, 58332, 31672, 65795, 239662, 77166, 190337, 3939, 38653, 96399, 34620, 237425, + 116505, 13199, 102812, 5543, 153497, 1676, 111555, 5249, 30589, 10038, 44022, 32064, 89029, 4156, 84630, 183016, + 38962, 20874, 41135, 5896, 181302, 141985, 28318, 41843, 43853, 109018, 101914, 14232, 78872, 97030, 13738, 107743, + 180301, 8364, 79955, 206838, 4786, 1843, 19029, 45663, 248240, 2387, 51445, 145895, 7401, 82908, 16435, 28522, + 106136, 4571, 2405, 140262, 112590, 26994, 36399, 3894, 77745, 48157, 81325, 9424, 19731, 7606, 25832, 161377, + 60880, 3462, 68402, 63020, 77789, 8989, 94980, 140615, 125748, 101040, 138, 61603, 135487, 19491, 9072, 42404, + 5975, 4730, 23228, 147617, 48627, 88470, 18481, 32183, 34084, 197974, 15306, 16406, 12419, 25554, 80789, 50074, + 215770, 77760, 67732, 15820, 47557, 8552, 32891, 3397, 254582, 51747, 37440, 13256, 10364, 19078, 197381, 38702, + 106495, 126239, 69247, 4048, 21856, 4277, 25578, 128895, 67539, 63586, 54687, 80647, 88981, 92208, 26195, 51852, + 38805, 50151, 28772, 79952, 21428, 21251, 116522, 15445, 48732, 44111, 50224, 95470, 42316, 106832, 48425, 377321, + 12149, 3533, 41847, 71691, 16078, 249001, 118133, 10711, 52808, 33393, 18850, 60881, 25327, 125536, 35507, 17128, + 51322, 25298, 64567, 14087, 33850, 17830, 26983, 37516, 51147, 63624, 6036, 75728, 12253, 42565, 30641, 60123, + 122354, 7767, 33831, 15668, 46077, 207921, 704, 228032, 56483, 154155, 19104, 43429, 254553, 40400, 19915, 39707, + 115417, 1959, 8797, 59126, 81834, 6291, 43802, 41057, 150991, 132620, 67404, 31385, 94662, 35042, 22728, 29984, + 86668, 80841, 166714, 23521, 7381, 18863, 127695, 23731, 12841, 34184, 955, 46179, 100650, 105059, 92227, 35881, + 18218, 34994, 30732, 65296, 15741, 79032, 12811, 2842, 22372, 120408, 11638, 298925, 68294, 83360, 60616, 1270, + 50705, 35353, 39160, 65700, 15535, 87701, 7971, 17998, 84660, 24834, 3600, 57330, 61887, 43556, 70547, 21033, + 22553, 123308, 92138, 46071, 72299, 43807, 86552, 3952, 31361, 45177, 11621, 157425, 24824, 87145, 1530, 1015, + 17743, 64397, 14528, 84960, 46820, 135812, 40268, 205321, 64288, 83124, 142613, 20892, 31582, 178130, 41319, 47604, + 77006, 38648, 45265, 69293, 111674, 38866, 30288, 90253, 116384, 11710, 162727, 119339, 30760, 74575, 99191, 114910, + 80920, 74030, 166787, 23839, 86149, 70396, 8817, 71462, 77192, 61144, 7550, 263557, 51979, 2741, 12376, 38498, + 79691, 27990, 88220, 46311, 60342, 115770, 32907, 654, 122805, 22347, 45779, 35595, 103800, 61077, 11173, 7981, + 240873, 127729, 60554, 100208, 160744, 278120, 46400, 47854, 233114, 14783, 28068, 50186, 78962, 21368, 149837, 32533, + 54920, 67698, 16575, 42220, 8608, 244187, 24441, 16118, 3484, 29636, 35155, 100272, 316104, 399, 13004, 70176, + 72548, 2188, 3176, 10044, 24337, 146534, 171223, 44154, 5088, 100828, 173780, 92915, 230040, 59854, 91355, 69382, + 21926, 88289, 10494, 133339, 10172, 99597, 53605, 17770, 36838, 15150, 30766, 12102, 26, 22751, 93985, 48775, + 86221, 110954, 26896, 56128, 83458, 19243, 10858, 11338, 102176, 1734, 27656, 45449, 12062, 47678, 227191, 104843, + 17571, 33218, 31175, 80, 41929, 75064, 823, 5915, 41170, 26266, 21858, 74328, 28428, 46729, 53037, 208149, + 68239, 44371, 128012, 14846, 41750, 121730, 939, 16024, 103930, 2667, 3749, 72822, 2634, 17905, 21653, 37065, + 18313, 12459, 26288, 15851, 53019, 237454, 82804, 43717, 34825, 6324, 223813, 34763, 97837, 74764, 131779, 54108, + 63115, 77477, 133465, 158834, 24606, 172748, 8241, 11219, 73157, 67543, 21979, 44698, 152474, 62783, 25538, 151168, + 14715, 17653, 20409, 10177, 91439, 51243, 33807, 21982, 37033, 28498, 20946, 82195, 109806, 89357, 35843, 62764, + 140259, 29524, 15905, 148965, 30668, 242609, 18782, 55072, 174760, 95402, 12389, 60205, 380, 39535, 99410, 68744, + 135597, 29770, 37761, 9074, 95673, 61075, 167448, 81798, 136073, 92495, 63964, 71292, 65073, 16100, 82788, 3903, + 134249, 666, 114606, 13925, 13829, 14923, 22844, 73642, 17279, 39192, 82814, 14279, 122305, 98412, 2819, 90185, + 4420, 211793, 88571, 343220, 46444, 31428, 94176, 75136, 10237, 43041, 121311, 109668, 64848, 79724, 95455, 406446, + 203623, 49760, 35347, 15313, 70728, 55604, 64355, 9274, 10349, 116949, 76977, 10948, 182885, 140337, 63627, 148647, + 65075, 60013, 4856, 3391, 24519, 53746, 165940, 8600, 25783, 64942, 35809, 14075, 40318, 2510, 34997, 36980, + 34139, 23025, 39457, 7315, 22222, 75794, 8923, 194881, 63394, 25194, 37165, 85475, 55266, 208468, 18378, 53662, + 102764, 38595, 7896, 34791, 41422, 49686, 92984, 25098, 20126, 17645, 88907, 226875, 65100, 60009, 15638, 21283, + 90408, 4537, 139878, 112661, 53640, 5071, 42553, 9995, 35128, 46262, 76889, 67947, 48932, 16991, 106940, 167117, + 11192, 66889, 6670, 104891, 38935, 1875, 45170, 3303, 96839, 772, 3134, 41094, 34782, 66145, 43963, 48995, + 39492, 21237, 117116, 33731, 19396, 265866, 122508, 109994, 41332, 31277, 72923, 726, 6250, 12016, 13536, 75815, + 5511, 102922, 12522, 133050, 19492, 24257, 18746, 2693, 51304, 63505, 129615, 231652, 25936, 33108, 79906, 94200, + 104466, 80492, 72337, 73422, 54099, 254560, 176028, 6993, 73771, 49079, 55319, 58712, 86115, 97967, 23109, 55938, + 5080, 244577, 48923, 66103, 7669, 640, 49551, 74043, 30891, 80537, 202612, 47981, 111700, 26871, 4345, 17399, + 13931, 293811, 135578, 107640, 25276, 30158, 17676, 15676, 72289, 37101, 1637, 43083, 135447, 37641, 14254, 111332, + 14820, 13404, 34584, 56626, 258641, 7240, 63894, 83112, 25265, 17841, 32376, 48491, 31005, 66732, 30950, 9648, + 281179, 112290, 34755, 61683, 75286, 5189, 100077, 59697, 393, 103531, 23185, 179430, 95359, 298178, 110282, 125995, + 14623, 78807, 24189, 26684, 13584, 47803, 47440, 29923, 6680, 25153, 12281, 81189, 101227, 5727, 57666, 53928, + 80173, 157148, 23623, 17510, 44933, 56582, 107749, 28680, 76666, 75185, 175076, 32262, 54542, 14210, 77349, 27496, + 13244, 83199, 3441, 55821, 39348, 3757, 3667, 123147, 458, 15000, 19818, 5639, 25379, 68555, 51878, 6205, + 109451, 7850, 13287, 9188, 134348, 386526, 16856, 19356, 81143, 264611, 26487, 30169, 6959, 42394, 96934, 7084, + 65554, 79211, 22545, 21576, 12027, 4118, 60397, 13483, 51311, 75590, 42156, 36096, 8716, 11493, 42998, 11218, + 57589, 275790, 7172, 33265, 140731, 69517, 43030, 8376, 28467, 43930, 2234, 31591, 23316, 47974, 14197, 146070, + 17272, 6751, 33924, 168150, 30458, 113416, 293380, 11766, 25980, 203311, 28924, 162345, 55229, 20334, 34079, 27402, + 77197, 13365, 186022, 69870, 83798, 55050, 364150, 25353, 28302, 1155, 109582, 70417, 114784, 7067, 16416, 132275, + 7428, 45143, 48146, 46692, 34548, 35154, 92593, 5358, 26241, 23637, 54860, 9482, 14712, 7966, 32576, 13535, + 39336, 35734, 47925, 187574, 103304, 90255, 22548, 13788, 18928, 36142, 63464, 150312, 54080, 263654, 319602, 6537, + 12870, 133946, 9773, 20050, 334, 130222, 30305, 136258, 87722, 40831, 167627, 13993, 15208, 85494, 50771, 220399, + 16895, 50769, 10053, 113498, 142098, 93461, 17165, 99681, 114262, 41550, 192972, 66158, 39820, 17436, 87519, 144390, + 83913, 82212, 14723, 8746, 57817, 78233, 11144, 30225, 28682, 86362, 276167, 25943, 7721, 38719, 161361, 102297, + 14900, 88287, 14336, 12092, 108672, 42339, 328, 10290, 11250, 44623, 111087, 145880, 62246, 20511, 67542, 263445, + 42849, 24396, 94945, 30646, 415188, 26446, 102124, 18065, 1724, 4925, 110914, 163915, 26555, 176996, 8050, 33583, + 24549, 11288, 16296, 29023, 25505, 6867, 86739, 11159, 26443, 84520, 68545, 10696, 107450, 65107, 90951, 10518, + 145899, 31404, 52435, 29234, 61035, 11336, 53944, 64679, 43528, 83757, 4052, 13189, 6901, 39247, 35310, 26976, + 60726, 185599, 8030, 4198, 65906, 57296, 259345, 122777, 267741, 2857, 142950, 19003, 21338, 112410, 33257, 200700, + 147590, 74901, 51360, 32601, 42079, 29847, 124456, 34389, 18924, 20790, 120555, 65991, 73017, 171882, 21281, 26841, + 135236, 5978, 4123, 303, 15393, 27267, 28700, 249892, 5206, 105391, 162130, 107419, 4026, 62796, 18843, 50664, + 84185, 9681, 10383, 108809, 1531, 34176, 8061, 39095, 5988, 39057, 7403, 4419, 113890, 60683, 85058, 11712, + 82647, 76332, 51237, 903, 303391, 133929, 25009, 138549, 7386, 175781, 132183, 3037, 69844, 21065, 30442, 4101, + 71611, 155271, 265989, 32740, 189865, 56230, 135927, 48500, 76523, 108510, 11776, 16685, 31877, 27734, 41614, 24689, + 13315, 15066, 48022, 4309, 19314, 41098, 90569, 30515, 198575, 24381, 154303, 42859, 32821, 78665, 30662, 14747, + 1928, 59755, 28149, 70209, 67641, 20901, 5264, 50251, 25913, 66241, 490439, 175537, 104475, 97516, 78264, 91266, + 103489, 23865, 183520, 34766, 3297, 275917, 146670, 25323, 70391, 25755, 49964, 164202, 18406, 31978, 16441, 52632, + 15446, 24429, 4215, 37736, 113347, 8883, 22563, 15500, 19295, 41760, 78521, 113283, 93790, 25764, 24081, 23658, + 27856, 43669, 81754, 11052, 1792, 147034, 105048, 59257, 167471, 86802, 148695, 15116, 116449, 115822, 22405, 24926, + 8541, 22171, 31801, 33192, 4408, 12297, 301197, 138987, 41757, 44743, 115490, 73003, 63233, 12310, 113745, 80287, + 25765, 1137, 45241, 12509, 86680, 100507, 15502, 82114, 64501, 29571, 9042, 4784, 27034, 836, 106118, 79642, + 24816, 19191, 71859, 10806, 34975, 35721, 20447, 33671, 6079, 126054, 58217, 78753, 4486, 35660, 45492, 39072, + 49693, 135128, 38873, 1595, 36229, 21988, 86413, 27520, 16917, 83041, 32578, 42649, 21581, 17612, 3706, 5582, + 62426, 61684, 21930, 147493, 27862, 16374, 25590, 69477, 11612, 15240, 18552, 19226, 54284, 19154, 205, 44618, + 35702, 62029, 11975, 135778, 194034, 34324, 9287, 92145, 355, 83533, 389, 11125, 24277, 28651, 33600, 110599, + 48262, 80091, 24087, 86535, 87411, 65839, 48531, 5435, 70504, 1680, 141541, 34304, 310164, 9214, 109239, 74125, + 118018, 80462, 100258, 37839, 12516, 18111, 111964, 15304, 47559, 22475, 250341, 55009, 43502, 72785, 26068, 56283, + 57433, 145320, 83034, 101357, 107139, 13166, 65124, 29871, 9290, 47434, 20163, 28721, 66533, 101179, 26384, 119496, + 80863, 26599, 33186, 50921, 14634, 49049, 8156, 90368, 34312, 71503, 2924, 84269, 91725, 54206, 70953, 60570, + 28606, 1961, 1020, 118183, 21342, 60064, 25713, 117531, 67241, 26343, 257386, 77026, 72355, 28646, 61026, 94224, + 43244, 94932, 4601, 230976, 375789, 103456, 58534, 48852, 37402, 24109, 241400, 52782, 174015, 1515, 35127, 236213, + 105070, 41444, 3868, 195472, 8342, 37810, 28026, 30469, 44167, 123934, 17110, 49127, 67494, 4950, 89802, 22448, + 1890, 32145, 62103, 193571, 16365, 8100, 2759, 59208, 11723, 30626, 54047, 111425, 271002, 34847, 30791, 102173, + 1865, 152807, 44228, 16334, 47918, 19851, 52637, 48405, 8350, 22131, 69413, 35540, 45564, 53848, 57537, 202520, + 27742, 16511, 37103, 9857, 25110, 80964, 59758, 10709, 125803, 10945, 60525, 12999, 8553, 3885, 21820, 165805, + 49504, 26657, 12487, 30455, 81925, 76254, 4388, 51128, 62211, 301599, 142773, 27276, 4534, 106190, 11978, 19483, + 15491, 115826, 50411, 58796, 19011, 32938, 119108, 220904, 80373, 67031, 70541, 4859, 206920, 6090, 19310, 22573, + 667, 55921, 9933, 6880, 102405, 3647, 62961, 136965, 128623, 63897, 23416, 79705, 245524, 144775, 47359, 10859, + 5553, 97850, 6803, 18191, 113309, 30019, 22922, 29253, 192739, 61644, 10879, 93327, 65766, 71215, 147457, 80167, + 19567, 55770, 29797, 29274, 22832, 23356, 42325, 44027, 261958, 72646, 19852, 9637, 29679, 36046, 49336, 14687, + 21293, 77708, 14113, 74893, 71134, 200672, 39308, 12740, 20962, 86248, 26029, 50842, 105123, 136390, 98208, 22087, + 24721, 49911, 106064, 73490, 860, 163439, 14873, 41067, 21752, 30501, 145265, 76566, 33448, 28437, 8815, 16951, + 18372, 74873, 29462, 32916, 157167, 37777, 218069, 57242, 94822, 93459, 63003, 77897, 35770, 25963, 42205, 118099, + 173224, 15519, 76989, 16637, 232737, 22211, 31315, 67805, 75729, 4140, 57334, 9310, 28937, 79865, 138213, 106821, + 46828, 51030, 76484, 117312, 28062, 12545, 71393, 159499, 25453, 210547, 151602, 22228, 5207, 75071, 53864, 71005, + 140366, 13537, 2178, 11825, 36665, 45071, 70308, 57129, 30652, 16553, 302183, 10738, 6169, 43148, 24995, 57331, + 67920, 86667, 244672, 341687, 150458, 19053, 961, 107389, 92040, 192870, 41097, 22344, 23186, 119577, 34986, 45018, + 184604, 177949, 6669, 18473, 92330, 10137, 20330, 189512, 20891, 13257, 66265, 48954, 176492, 72915, 219860, 2494, + 49427, 18529, 56158, 30214, 27828, 171123, 69463, 40254, 38305, 23967, 79164, 66024, 42495, 299257, 23031, 106341, + 143982, 353, 39736, 75709, 49560, 70040, 243406, 1642, 25503, 56434, 81502, 48303, 90043, 52859, 24462, 43046, + 29747, 41457, 23434, 42918, 65328, 52708, 5329, 21975, 47830, 3326, 160281, 95290, 12932, 95952, 35520, 107324, + 11068, 52610, 109869, 64849, 77721, 9674, 61370, 154578, 9003, 27427, 87582, 116020, 25213, 95646, 34677, 3719, + 94205, 2145, 19568, 65295, 140426, 3088, 26113, 131686, 46090, 188040, 30031, 72073, 89945, 2538, 23463, 34360, + 138173, 3342, 84724, 64829, 192691, 8206, 251775, 2536, 33329, 64010, 2755, 48205, 112232, 33297, 244729, 27663, + 129905, 107744, 55337, 67101, 35709, 152617, 74645, 44141, 27514, 12925, 107358, 33190, 1841, 66538, 7298, 34436, + 19957, 54584, 3634, 41173, 31411, 2298, 3434, 77461, 127476, 54373, 77688, 7987, 53572, 15128, 19113, 176061, + 17497, 39049, 101234, 59914, 173549, 48281, 54139, 65147, 55063, 16371, 43136, 40263, 175135, 13721, 69771, 59399, + 19841, 1955, 57439, 88361, 69314, 130279, 804, 37567, 5192, 185175, 75166, 10500, 237921, 127018, 7558, 35337, + 117660, 21372, 36787, 27678, 150697, 7, 190870, 106339, 4060, 7260, 122007, 5881, 273045, 63325, 39801, 38618, + 50414, 113953, 105525, 17559, 98940, 56463, 347332, 34915, 65348, 25837, 82591, 5365, 153665, 27182, 7831, 15055, + 164423, 1182, 30831, 177372, 58804, 5448, 49128, 44734, 156695, 4975, 125400, 91561, 48994, 97252, 49285, 17162, + 213928, 127791, 49987, 50768, 86036, 12840, 111058, 253850, 28608, 197563, 19740, 127785, 8355, 34689, 65656, 32199, + 39574, 8110, 23600, 97524, 34540, 38651, 19006, 29152, 16927, 100216, 30893, 172304, 135680, 31450, 91503, 54177, + 18374, 32795, 63764, 459294, 151587, 85350, 39064, 13067, 10830, 3717, 20553, 32482, 53805, 108785, 109353, 20145, + 16878, 76255, 16289, 14152, 16623, 3446, 23337, 31309, 4282, 24663, 64821, 61752, 48030, 64655, 21808, 264145, + 8537, 50728, 25184, 49171, 14986, 13324, 23567, 199062, 46102, 179857, 99718, 369654, 13062, 27072, 2232, 105686, + 72897, 219385, 64202, 22442, 72, 52447, 22847, 94762, 33050, 52976, 8735, 2293, 108227, 50715, 42136, 12707, + 39451, 45981, 114988, 190349, 45935, 22798, 12654, 1, 651, 11355, 22585, 15841, 113320, 18682, 87649, 22561, + 40535, 140869, 61447, 16658, 95176, 80270, 61544, 83797, 57450, 101532, 133714, 89999, 48843, 172813, 18252, 163124, + 5003, 103269, 9853, 67492, 19019, 55271, 3109, 55823, 10407, 119899, 97338, 54114, 211163, 4927, 123086, 69260, + 3848, 55061, 18449, 12690, 1068, 37710, 26424, 11375, 4988, 41383, 92404, 48881, 32091, 48305, 36150, 113778, + 30095, 105405, 16612, 40433, 41692, 73917, 51729, 55139, 15099, 30180, 50, 16916, 43602, 95240, 47258, 86059, + 107434, 94751, 15026, 33649, 50744, 49046, 74109, 13167, 7627, 11804, 18035, 3335, 171349, 35806, 44194, 37671, + 16313, 34545, 198682, 35794, 150832, 210760, 258621, 12579, 352665, 110221, 193929, 21773, 207750, 141990, 78065, 65827, + 33937, 281, 49827, 8372, 38256, 111292, 55786, 57932, 51091, 10740, 12648, 39213, 156000, 72468, 27361, 213358, + 87889, 22207, 42213, 35711, 90663, 88229, 37662, 37545, 84175, 5983, 52865, 9162, 24908, 28484, 109135, 3656, + 114900, 154191, 40016, 143364, 50365, 4998, 47423, 91888, 31494, 33385, 89791, 113590, 83829, 74958, 6063, 23411, + 5398, 3346, 29188, 43992, 169342, 124619, 152146, 38176, 47521, 837, 5847, 40491, 54818, 14886, 64782, 79830, + 18935, 46064, 22834, 11304, 8356, 14908, 14164, 58309, 43094, 59761, 58932, 55478, 41212, 27362, 8157, 45308, + 174536, 290996, 677, 204177, 10082, 87199, 60656, 99512, 92550, 18666, 17670, 8755, 6678, 78663, 12108, 219237, + 60614, 81551, 23867, 117589, 23355, 14754, 99693, 35914, 69721, 75856, 71852, 97445, 14796, 53501, 37755, 5823, + 34149, 11053, 56010, 32326, 128830, 80883, 474, 3312, 58187, 4593, 94897, 82655, 3179, 117179, 34370, 37073, + 208, 174, 40568, 42678, 40325, 118866, 28501, 3518, 28399, 91754, 79629, 270203, 225029, 103041, 171673, 19198, + 401412, 202372, 71959, 27441, 51150, 57934, 46575, 551, 31580, 48734, 52559, 6830, 207268, 88303, 10399, 26375, + 6657, 26942, 1499, 28435, 10993, 84614, 864, 33684, 69818, 63313, 138059, 44306, 64282, 22203, 52406, 127830, + 289845, 11019, 2908, 36009, 23308, 8408, 38414, 42453, 12961, 116672, 9638, 175093, 38447, 99982, 7614, 4603, + 6681, 54049, 103103, 12820, 52944, 2652, 87605, 137098, 31855, 44982, 31388, 16335, 2572, 234999, 76439, 59626, + 47646, 105458, 231, 16630, 120728, 71649, 54479, 42672, 179148, 62338, 5367, 4698, 37240, 85883, 273485, 122580, + 45196, 6452, 17224, 35656, 218274, 532, 77135, 92225, 4816, 24612, 23330, 78494, 3695, 84373, 30447, 293164, + 21961, 19227, 40712, 50432, 50084, 83383, 130654, 3512, 35209, 106119, 26859, 2775, 18073, 188766, 9641, 22040, + 51452, 7828, 120628, 59247, 27004, 7212, 84542, 50515, 6100, 130271, 27415, 45596, 33941, 106546, 4823, 107962, + 1377, 42166, 117980, 25577, 84831, 24787, 184967, 17471, 171214, 62502, 4444, 8334, 85, 27407, 295919, 244072, + 141510, 43179, 145423, 52704, 9078, 33296, 18231, 71008, 99227, 13981, 68573, 4322, 32610, 51176, 165546, 3853, + 6417, 145489, 23086, 27479, 11718, 56566, 19653, 100740, 49868, 121955, 56420, 11535, 65579, 132995, 125548, 43942, + 87902, 58981, 4510, 84294, 73018, 226515, 1295, 68198, 49062, 157567, 27234, 124146, 46280, 100486, 144184, 15600, + 61742, 26572, 61714, 65125, 21512, 7799, 35874, 6311, 40862, 35522, 45414, 16108, 107733, 43364, 9206, 73819, + 15941, 51689, 82329, 40065, 29168, 48562, 85845, 69609, 157765, 60708, 25387, 1180, 144919, 159797, 25726, 214431, + 14487, 5968, 68537, 109664, 5767, 13490, 63443, 104676, 158014, 10404, 26593, 10161, 140070, 96476, 96798, 10196, + 7241, 29156, 51314, 97628, 573, 118109, 8622, 3106, 71584, 57894, 84024, 11036, 16921, 66038, 61545, 106441, + 223566, 16117, 74626, 3336, 40331, 47655, 20982, 117267, 179473, 76397, 121704, 23368, 35081, 186150, 1889, 47653, + 47926, 33122, 15734, 26894, 140885, 14802, 76951, 41988, 41508, 57629, 16634, 12405, 52104, 20107, 218288, 100668, + 59180, 73629, 1683, 30932, 42310, 64739, 20003, 6633, 32811, 26700, 39873, 153638, 29048, 2831, 22955, 8961, + 123517, 244356, 25796, 26746, 102413, 144572, 12002, 20480, 80208, 92037, 145215, 65587, 10104, 70587, 35982, 10208, + 14746, 188951, 116180, 117036, 12649, 257536, 49699, 32220, 153641, 10918, 10962, 51792, 126022, 13715, 104110, 23594, + 37965, 15247, 6442, 44822, 113017, 28398, 13830, 44800, 4171, 120616, 5418, 1810, 83, 42459, 4381, 81522, + 142592, 107242, 4170, 85703, 2809, 7049, 62349, 190193, 6362, 36642, 21195, 33097, 50416, 52066, 84992, 65769, + 71323, 20902, 52748, 114648, 116894, 25884, 34351, 102634, 260776, 19638, 86892, 17434, 16204, 19854, 106540, 27954, + 1524, 13745, 42151, 138947, 5760, 153807, 35075, 95356, 30351, 27161, 68708, 53500, 12658, 22077, 63851, 8487, + 20703, 57740, 44334, 64734, 54403, 39682, 77475, 5602, 36083, 1112, 36181, 71932, 45408, 99180, 206226, 42336, + 74772, 77663, 25805, 117083, 4946, 39476, 36769, 30289, 14485, 5872, 59638, 72213, 50759, 23451, 882, 2453, + 111222, 168615, 130208, 48836, 10890, 90002, 55698, 21422, 2195, 35834, 39131, 16781, 167147, 16091, 54925, 18399, + 92962, 80011, 5820, 4726, 130534, 187899, 869, 40302, 16283, 28616, 86006, 14823, 177256, 25701, 70837, 29786, + 35016, 19926, 80067, 4711, 15472, 93684, 2584, 58032, 210156, 70971, 75498, 15685, 151187, 60994, 38213, 13471, + 73922, 9338, 117718, 24543, 117691, 15713, 45967, 200243, 43250, 36553, 35694, 36433, 52051, 152826, 305512, 217989, + 37392, 40189, 4153, 56219, 24811, 51616, 37703, 87103, 24358, 84298, 167734, 60608, 30830, 95114, 82423, 123075, + 5775, 16326, 137007, 23746, 818, 184283, 59155, 49161, 21969, 92570, 27322, 24660, 1476, 194447, 116982, 30577, + 127322, 117428, 1856, 80745, 151783, 5171, 15901, 75451, 58392, 49455, 93446, 42926, 31021, 17030, 17243, 171279, + 106913, 15354, 115117, 51694, 65215, 88371, 23841, 28644, 89407, 71198, 6973, 57127, 90802, 67682, 21453, 30346, + 28531, 59792, 72619, 106195, 11690, 597, 21636, 30078, 20234, 8145, 91408, 50011, 95249, 25250, 66246, 24442, + 44602, 12103, 41001, 105897, 37256, 44489, 85248, 1331, 18707, 29983, 310182, 6411, 11928, 10116, 19299, 122916, + 5161, 82625, 56098, 136518, 4410, 33338, 119068, 31371, 26571, 52839, 11442, 358, 51903, 115795, 48253, 212226, + 49768, 72313, 32154, 54738, 22008, 16766, 174325, 98378, 25252, 9732, 16533, 147195, 65780, 41940, 24564, 81099, + 209499, 21378, 137617, 184321, 68769, 172072, 71325, 81618, 203726, 24974, 21300, 111798, 13249, 30461, 47901, 78074, + 137363, 96937, 205703, 15259, 48845, 38294, 28061, 109460, 86823, 28722, 44363, 19999, 6658, 142277, 14939, 11150, + 5674, 45392, 60588, 177764, 31881, 6786, 145293, 13598, 1083, 12784, 3617, 14433, 1823, 25033, 79112, 70251, + 108676, 88876, 67887, 11458, 34518, 12199, 148504, 65495, 166752, 78027, 54905, 18762, 13791, 20914, 58692, 1568, + 14287, 15068, 7216, 15244, 91576, 191867, 58273, 3830, 91429, 78507, 84897, 9770, 8665, 7954, 43039, 48860, + 11529, 61697, 166056, 55960, 26401, 61415, 290831, 12539, 16191, 30889, 13589, 1191, 91972, 41144, 4955, 34048, + 30964, 87299, 107280, 64425, 5254, 43169, 46627, 18402, 28486, 30816, 67369, 1564, 54697, 41405, 16000, 32524, + 79613, 30190, 43938, 8057, 66520, 53870, 1494, 247505, 18447, 16053, 29278, 66743, 22870, 25668, 1648, 14080, + 45203, 1341, 40989, 119871, 194466, 122534, 8385, 58819, 22822, 35970, 12729, 29360, 51703, 27032, 51912, 51956, + 12278, 36617, 79242, 39507, 76716, 85023, 73180, 18140, 44595, 125017, 191485, 174629, 73455, 77570, 220522, 125113, + 33546, 90187, 62766, 35279, 12235, 8675, 15151, 50393, 144843, 26013, 205214, 46310, 36154, 69776, 28572, 32563, + 51247, 38454, 4595, 42074, 11116, 86835, 30706, 10273, 33040, 34204, 54246, 91737, 3180, 77652, 106293, 106121, + 225753, 62203, 83244, 49829, 60864, 33244, 3262, 132227, 1972, 167168, 175800, 113557, 28469, 1342, 99125, 98666, + 12891, 8033, 119055, 3277, 28879, 37357, 275688, 62785, 10338, 60445, 97431, 99394, 144157, 1870, 20794, 59985, + 56294, 1569, 12614, 65686, 353058, 24023, 105292, 40234, 38302, 59113, 20587, 39754, 41447, 7733, 28382, 149537, + 87532, 70154, 27770, 8584, 110616, 28877, 50839, 33339, 27065, 8349, 41578, 41373, 168438, 10230, 58202, 18179, + 6557, 87189, 41859, 112308, 1213, 37229, 12748, 127395, 50804, 25519, 6813, 29126, 144643, 51945, 3761, 173270, + 24817, 37177, 11538, 1953, 2390, 71610, 55025, 12286, 136531, 8290, 7081, 13438, 38174, 12201, 368643, 56955, + 247513, 86715, 29189, 151151, 16190, 44518, 9116, 26301, 4059, 29547, 121363, 528, 122791, 104758, 128283, 132963, + 131994, 18283, 17120, 57082, 137430, 286470, 90537, 63450, 39506, 73884, 58318, 16044, 57650, 17259, 42080, 17885, + 16305, 157015, 93813, 43437, 5188, 134150, 32055, 268669, 54309, 84632, 18425, 114608, 106128, 82465, 25150, 81372, + 20628, 50827, 203900, 88756, 88071, 113318, 88552, 32344, 67394, 25784, 120662, 65041, 395446, 1313, 179364, 2878, + 250285, 16496, 42810, 142259, 66176, 14834, 29115, 136061, 91254, 103667, 12871, 26008, 1399, 9634, 6954, 97146, + 114196, 292674, 65716, 14216, 43915, 106501, 379, 35470, 60230, 24709, 71955, 28003, 44853, 42762, 19842, 9247, + 27206, 76172, 35445, 42656, 106353, 30864, 56216, 217302, 43013, 490, 12455, 125743, 18733, 112917, 66668, 5890, + 345105, 38120, 9856, 28648, 226453, 13944, 99130, 54004, 51202, 214051, 47536, 22937, 16607, 40104, 54194, 4979, + 57106, 15086, 23012, 12071, 117175, 174267, 29878, 59251, 35492, 196132, 120077, 81399, 10476, 19539, 129457, 31908, + 89598, 42460, 90787, 28424, 127439, 6776, 101077, 81013, 15187, 1074, 58103, 66003, 39624, 68595, 18810, 173127, + 13688, 6576, 66630, 43484, 61570, 92693, 65418, 85754, 10615, 177935, 31294, 91906, 31111, 386524, 52324, 16388, + 59370, 52508, 156372, 25357, 6238, 72256, 41599, 57828, 175252, 163986, 132645, 50076, 32143, 95350, 15564, 103443, + 224492, 75148, 26023, 120071, 41388, 19532, 110427, 22508, 95408, 89126, 17624, 37562, 34384, 9140, 91145, 109567, + 148238, 18379, 47470, 5638, 78307, 70465, 82451, 53859, 38959, 18925, 14088, 22217, 9340, 26777, 74821, 42124, + 160091, 16523, 3150, 97181, 61443, 8097, 65561, 68601, 15737, 115420, 25095, 57655, 11216, 70875, 87640, 78471, + 41244, 28465, 55017, 134190, 170, 58246, 16739, 39956, 38299, 255505, 2797, 2174, 102443, 13841, 69822, 12621, + 113097, 6991, 123270, 37586, 26382, 47496, 42833, 10023, 14027, 38076, 52804, 80220, 33707, 4788, 3121, 7610, + 3957, 167985, 5094, 37233, 76300, 62786, 189431, 11488, 66160, 1236, 76849, 5333, 19431, 42643, 23661, 46201, + 18900, 8417, 18568, 111327, 6952, 44621, 24495, 38741, 1717, 138255, 22782, 46607, 108656, 236097, 24621, 9067, + 82206, 38888, 253672, 45369, 188021, 74422, 200471, 3792, 257335, 14028, 151249, 5429, 27295, 141619, 22966, 27219, + 43999, 105930, 97394, 24617, 41210, 3333, 88262, 22024, 31777, 58259, 8812, 91559, 46956, 22151, 60598, 161311, + 57457, 123650, 86473, 64439, 12657, 10686, 130688, 112742, 11489, 53274, 26714, 21670, 15697, 30443, 104596, 7868, + 48060, 22775, 3022, 19869, 204748, 16977, 184709, 89313, 53583, 83928, 92875, 99194, 82422, 96190, 2556, 47490, + 284790, 12772, 5841, 48964, 30503, 33825, 99246, 251304, 137341, 36338, 22912, 3614, 8120, 31432, 14001, 2727, + 19615, 36074, 75714, 22938, 220311, 52593, 32987, 17971, 15991, 102877, 210170, 136379, 10217, 43348, 155559, 9056, + 63424, 28650, 29017, 9663, 9808, 49301, 50859, 10641, 67431, 17280, 61331, 20739, 70976, 97391, 58235, 36525, + 98221, 122956, 57506, 98979, 4491, 86694, 28324, 129, 15177, 9809, 3222, 215310, 28535, 4761, 16001, 1184, + 144789, 181348, 54083, 88078, 751, 22452, 65081, 1577, 13230, 27685, 98822, 56681, 2394, 90263, 54478, 144599, + 1504, 78572, 173001, 99606, 33977, 33470, 29437, 39886, 132104, 10699, 34506, 36978, 30316, 13646, 16311, 29262, + 22230, 50283, 49086, 343445, 931, 13052, 125899, 139325, 97193, 24009, 38257, 76027, 185240, 47587, 137522, 115144, + 24826, 38532, 19149, 8495, 22687, 75105, 130036, 15268, 174322, 68514, 245144, 17081, 15307, 34585, 208142, 75209, + 22988, 36011, 65, 2906, 1390, 60888, 44865, 144040, 188745, 118480, 95778, 32437, 180325, 4138, 10609, 92925, + 29580, 8808, 159680, 42631, 59068, 29860, 171355, 10899, 74903, 33949, 320605, 9425, 18994, 26854, 7737, 53509, + 29195, 107306, 35880, 21197, 79, 68771, 286937, 4362, 15436, 42681, 71303, 124778, 7622, 25028, 9618, 122572, + 38462, 11060, 66457, 65269, 11566, 72952, 5073, 71968, 138710, 28743, 12069, 66022, 44828, 82002, 156524, 81292, + 45774, 14165, 218072, 86389, 37768, 116234, 37323, 222673, 99236, 417011, 6380, 170851, 68137, 22809, 50851, 17147, + 84083, 118504, 78497, 64504, 19282, 56977, 84684, 68011, 22698, 100149, 2846, 125107, 17134, 46339, 16369, 72262, + 74807, 15652, 17984, 99115, 126662, 49499, 64245, 224198, 173497, 81277, 63478, 3449, 46248, 2829, 31143, 91485, + 16938, 9355, 21751, 89231, 119735, 2651, 2158, 25221, 3212, 1095, 134321, 26633, 28292, 72271, 10874, 18895, + 213652, 343495, 36158, 6930, 49013, 9714, 53844, 16595, 9975, 99720, 38334, 23140, 32180, 298162, 284394, 20189, + 45660, 51804, 12038, 74719, 86250, 44131, 68813, 48629, 4801, 41574, 219878, 76411, 68788, 91859, 17071, 199893, + 95490, 13890, 126132, 21590, 77482, 5070, 117208, 183553, 113751, 775, 118421, 47980, 11994, 16510, 60560, 22757, + 44624, 41900, 22489, 161977, 94452, 40768, 256639, 97607, 46839, 15049, 48016, 183793, 128497, 40127, 59466, 43034, + 100316, 61744, 20099, 72276, 5798, 4254, 61106, 151277, 58588, 78938, 208785, 23350, 73184, 13401, 114456, 168253, + 202987, 128773, 32481, 9314, 65417, 80566, 15061, 20781, 37790, 80269, 18985, 16154, 88524, 11484, 16349, 5922, + 1606, 101590, 83867, 4032, 43156, 17265, 40946, 123245, 97964, 46724, 2142, 201438, 105717, 55537, 40251, 107387, + 34947, 130879, 26300, 2025, 11203, 27400, 9384, 6700, 100060, 93137, 120697, 32781, 37742, 97514, 147819, 50972, + 130074, 43696, 152282, 11325, 93653, 25846, 60051, 100451, 107799, 99294, 5187, 187837, 94311, 19648, 17481, 47149, + 196106, 2484, 185532, 68892, 41347, 6476, 26576, 262, 8035, 144425, 16194, 7546, 10780, 99032, 192083, 18268, + 16390, 38046, 139599, 36447, 27883, 48800, 8802, 104301, 118236, 16610, 9043, 30215, 167395, 15722, 14540, 10143, + 1979, 18303, 245965, 6606, 25006, 56388, 720, 40122, 19375, 26986, 4175, 5283, 31628, 70617, 156858, 13338, + 18916, 50924, 158448, 13314, 144723, 40846, 148751, 33355, 78502, 66354, 52938, 44935, 114047, 29390, 83010, 31740, + 103107, 187158, 28282, 6840, 86492, 173457, 46403, 22614, 107686, 143217, 20089, 170121, 5844, 9860, 56485, 104630, + 20934, 42133, 9301, 19064, 206963, 93906, 29729, 27462, 23556, 248023, 29615, 24218, 22591, 27525, 19222, 62444, + 16562, 40084, 90324, 40232, 146333, 178921, 45549, 11142, 20167, 301568, 34164, 125423, 10471, 17862, 4749, 774, + 117434, 30213, 12597, 85041, 33085, 58865, 17338, 4578, 2863, 16515, 49743, 2267, 9740, 64838, 32867, 305033, + 36669, 34833, 20474, 42789, 41849, 24106, 210964, 124297, 37271, 24216, 53900, 123495, 22790, 8477, 175065, 22886, + 18209, 95189, 3313, 32543, 28979, 29761, 127609, 71172, 8231, 87016, 63834, 20159, 12952, 70904, 466787, 101605, + 54408, 2160, 17597, 57212, 21731, 165012, 21316, 33552, 25130, 56209, 46615, 46375, 45208, 106318, 31681, 64073, + 55748, 7104, 76381, 85964, 138120, 4075, 21570, 28070, 75826, 73539, 7912, 79024, 414, 177899, 313993, 67507, + 29593, 5743, 4806, 12800, 9925, 25560, 9189, 117626, 292865, 50234, 102480, 16382, 25999, 50641, 18440, 9929, + 683, 55242, 2340, 1064, 123149, 61826, 15245, 38280, 7036, 24794, 44030, 43924, 92159, 34247, 66141, 23809, + 86055, 215911, 128281, 150909, 61827, 53182, 142185, 14010, 103680, 51751, 108481, 22354, 23176, 13327, 14346, 152541, + 54918, 99104, 95228, 63611, 58466, 81038, 32483, 69723, 57578, 44054, 189180, 149427, 13305, 19749, 43628, 89334, + 5709, 43087, 18148, 4104, 86479, 50105, 64469, 20382, 16697, 4708, 14117, 130911, 31064, 73543, 33459, 45627, + 17660, 15860, 57462, 86199, 200919, 78755, 79677, 80038, 10770, 87019, 8576, 17552, 49793, 46030, 21495, 35725, + 33423, 27589, 152364, 6318, 32370, 142933, 34912, 78214, 52047, 54699, 36052, 229203, 16488, 20327, 25789, 14697, + 62555, 29116, 9656, 6836, 6459, 16067, 47438, 81922, 8426, 32236, 21951, 67133, 83493, 104694, 49662, 4774, + 7763, 74850, 270584, 335979, 59725, 82959, 82821, 18110, 82812, 14354, 2193, 9843, 18628, 69780, 24991, 112338, + 67760, 191557, 92348, 79071, 79405, 72842, 11351, 56088, 68557, 139675, 23222, 148134, 9612, 12610, 21344, 25747, + 7673, 584, 17873, 39734, 28102, 18328, 10063, 14720, 56517, 1902, 69798, 38307, 69620, 33351, 1174, 19948, + 171797, 67288, 84834, 16123, 32458, 25946, 172250, 8199, 29541, 28207, 15618, 8731, 15870, 23596, 47369, 57922, + 81109, 26904, 26073, 8326, 32080, 57471, 44892, 162057, 207644, 334076, 10101, 4119, 71495, 49601, 2592, 19742, + 21202, 14849, 98354, 61825, 11039, 158223, 75426, 119901, 91036, 68746, 116495, 8557, 61230, 102302, 14765, 75658, + 2810, 4942, 28526, 36256, 130800, 67752, 202742, 33081, 32260, 193926, 185696, 4064, 4613, 295863, 166466, 13260, + 60590, 1252, 145391, 2657, 37112, 87184, 227365, 8194, 75214, 88155, 115530, 90924, 33979, 90533, 27556, 51339, + 126402, 49225, 196178, 34452, 155062, 4813, 17478, 33954, 30642, 120974, 35852, 38833, 63875, 31380, 62028, 58381, + 12810, 7419, 98274, 1977, 194463, 145760, 23510, 116833, 82799, 19072, 2433, 145655, 47664, 4834, 69147, 46751, + 16725, 33328, 38665, 115531, 36685, 76090, 11537, 18743, 43367, 17948, 23978, 41370, 61099, 40095, 66518, 999, + 449, 217319, 6688, 250897, 172150, 20516, 11330, 20451, 102867, 21452, 159960, 15660, 21691, 82391, 6601, 43312, + 301838, 29124, 21637, 110211, 36745, 105335, 60833, 98115, 7130, 2470, 75962, 2011, 18671, 50489, 79569, 101266, + 57316, 81095, 53258, 13308, 34852, 17013, 84541, 47478, 38034, 23762, 162120, 178016, 54182, 33123, 52028, 72197, + 35578, 4602, 243630, 88186, 65900, 67107, 5029, 138288, 99486, 1235, 6540, 165347, 19771, 47835, 318100, 22891, + 3456, 21803, 91103, 57561, 2658, 54417, 30476, 7012, 16914, 55333, 21913, 180607, 99866, 184639, 7485, 8405, + 28390, 37172, 89244, 53674, 28109, 98360, 69082, 3525, 8262, 79773, 254797, 87253, 21147, 105791, 15807, 58442, + 34353, 98558, 30931, 80675, 20006, 3002, 81642, 11376, 4228, 91457, 8547, 21430, 137085, 33238, 42307, 3087, + 1675, 66687, 47814, 34117, 203023, 131032, 24008, 5970, 283196, 124604, 83088, 60714, 198286, 26339, 5149, 82518, + 214375, 8762, 21409, 25932, 163329, 13237, 37495, 3608, 290603, 72236, 1508, 11575, 152574, 55633, 156361, 32414, + 40471, 48043, 3556, 2415, 83506, 9556, 79122, 233954, 30068, 33325, 6305, 159939, 14730, 53878, 89577, 30054, + 23177, 41063, 32980, 17345, 131539, 217504, 35311, 15300, 34759, 144987, 54877, 46496, 27668, 5784, 24491, 1354, + 32178, 129844, 14953, 7360, 71896, 107476, 206892, 65803, 104799, 60213, 3795, 77961, 116305, 72186, 184835, 52495, + 85430, 98086, 108950, 22959, 119262, 214032, 33931, 102185, 42860, 161725, 32444, 24541, 25160, 41398, 6650, 202950, + 8911, 27523, 50156, 13935, 23428, 255875, 23753, 49759, 49437, 771, 101855, 224178, 105322, 141973, 32780, 5494, + 6519, 83915, 103464, 195927, 16203, 18899, 2849, 150029, 6349, 3289, 4814, 219, 74711, 59509, 333, 40550, + 1230, 49476, 28787, 6325, 38045, 10647, 173625, 26321, 8540, 19101, 23643, 21796, 75165, 98886, 256858, 8390, + 44736, 107620, 67566, 91614, 25909, 54320, 31937, 195737, 51026, 52019, 46128, 10676, 317034, 7784, 41102, 123264, + 4984, 106475, 31610, 19260, 32281, 83653, 4280, 61891, 91312, 19136, 38931, 76940, 27060, 33501, 126832, 48333, + 44431, 81276, 41771, 130533, 17817, 6320, 38313, 928, 45363, 59120, 177473, 41182, 155937, 135020, 126653, 32047, + 239085, 115649, 82912, 3416, 35697, 345331, 53591, 16649, 59784, 39055, 46432, 28477, 91993, 8200, 97534, 6307, + 29531, 9129, 30788, 89098, 126740, 20671, 133582, 65905, 213757, 1632, 18153, 20878, 76560, 55987, 68969, 1600, + 167776, 51365, 34575, 216355, 285273, 37934, 49689, 21386, 24262, 69390, 24454, 75939, 8237, 18742, 88250, 165234, + 65030, 85487, 44653, 10365, 41160, 2784, 164637, 7275, 74437, 817, 5045, 54742, 48804, 217409, 12001, 99489, + 118916, 8909, 10151, 74282, 13159, 165410, 3506, 39017, 37842, 24440, 5032, 93366, 1031, 93948, 42413, 34930, + 75349, 36125, 57529, 29308, 1478, 45294, 1328, 29873, 11655, 72323, 80218, 16686, 108777, 112357, 19468, 161527, + 23435, 67822, 30370, 4433, 277425, 199425, 1173, 8369, 101734, 76516, 110263, 4965, 67469, 27648, 64330, 158915, + 70231, 148349, 33642, 19100, 124711, 6240, 206630, 5766, 43532, 60290, 1618, 11261, 28514, 49764, 75380, 44379, + 65526, 33015, 1566, 161773, 54956, 37344, 69904, 6421, 1000, 17254, 11877, 7155, 21882, 13912, 9792, 134, + 17728, 212180, 90771, 66606, 25302, 43754, 11818, 134151, 40952, 12919, 28325, 57470, 52214, 30361, 5898, 7913, + 149632, 18095, 212017, 195480, 1999, 139, 84069, 3822, 2111, 116190, 22381, 104936, 3259, 19369, 7470, 4564, + 63362, 84396, 244911, 82844, 89961, 73711, 23902, 88689, 220561, 81148, 100516, 124589, 39777, 153793, 37780, 13806, + 26335, 4176, 56333, 280949, 9063, 9260, 69363, 258594, 10572, 107880, 12115, 33299, 12416, 68082, 27837, 184178, + 34551, 83293, 68854, 109274, 34623, 9210, 18491, 59555, 38604, 267, 8192, 6400, 24723, 29696, 82525, 68604, + 5947, 72996, 15729, 703, 15588, 23700, 2015, 100398, 69927, 427, 20207, 148402, 66252, 2099, 146853, 12510, + 119177, 37939, 48402, 172082, 69173, 242876, 15286, 133076, 46629, 9996, 20910, 33571, 28714, 132255, 11444, 47791, + 70715, 103704, 9226, 28482, 212408, 75092, 6197, 29216, 20521, 24, 52569, 5853, 406913, 21243, 31218, 77868, + 74380, 146453, 7607, 72181, 11716, 15373, 26582, 8123, 50659, 30590, 227825, 66454, 50862, 49529, 80294, 15517, + 37009, 35230, 69063, 80260, 88460, 38472, 63246, 37205, 130101, 137671, 14972, 60171, 7210, 90428, 50245, 64301, + 53853, 21012, 116299, 19943, 538, 102919, 143609, 50795, 65120, 122155, 20760, 41285, 151950, 28489, 62634, 48588, + 55806, 151533, 4795, 3053, 163748, 44956, 565, 152058, 52837, 23981, 76468, 97083, 13153, 60576, 2112, 50486, + 21100, 377, 192917, 29902, 16674, 14359, 42767, 170627, 64536, 35897, 66424, 6902, 6091, 127107, 4355, 121366, + 138201, 65773, 66108, 41998, 44837, 63222, 69586, 36291, 58547, 23085, 14181, 135294, 3723, 40961, 35006, 126987, + 163, 9211, 49788, 117861, 2177, 37726, 91665, 22613, 32288, 24902, 24789, 76868, 85454, 74752, 103374, 11683, + 34033, 48129, 1456, 23503, 8497, 70596, 92766, 70637, 14282, 304999, 76392, 9980, 25742, 4216, 140344, 193566, + 10535, 16591, 137916, 20347, 10741, 5439, 17749, 74636, 79559, 244434, 10353, 2254, 117493, 6879, 36582, 273890, + 243787, 15483, 5037, 43308, 49337, 29065, 64416, 85528, 100718, 19024, 222754, 60476, 79495, 44751, 64434, 4020, + 40139, 30091, 121039, 83627, 42956, 12277, 115688, 38864, 7551, 37316, 31576, 348, 55433, 10897, 8383, 89713, + 15421, 4329, 42444, 12217, 31509, 48867, 30445, 38228, 23034, 8090, 37931, 30345, 45081, 21129, 36808, 88429, + 547, 39635, 34098, 148415, 61176, 52774, 24919, 16366, 53434, 13434, 146264, 79719, 328001, 5483, 62687, 73315, + 8470, 79268, 19141, 72096, 36263, 44493, 236350, 267628, 30145, 211091, 25890, 14437, 4519, 17070, 79714, 73443, + 74173, 53239, 98936, 72193, 53935, 17849, 592, 6437, 11845, 802, 96206, 13472, 73774, 36519, 15404, 33551, + 60211, 17322, 196495, 29339, 78025, 276332, 54124, 171051, 3, 52430, 53849, 77154, 4102, 16020, 8709, 109741, + 73782, 54762, 26431, 7665, 109293, 2201, 111613, 5780, 315332, 245103, 65577, 66474, 48412, 162153, 10534, 61430, + 26683, 61829, 32733, 13780, 13714, 81304, 58398, 119619, 6865, 107753, 132039, 172363, 20128, 121666, 235595, 131904, + 18490, 178167, 57539, 35059, 104141, 15949, 75689, 5299, 325, 57947, 48755, 8362, 39470, 20406, 30082, 13818, + 171970, 31118, 19942, 97627, 115860, 172133, 40888, 75047, 15707, 107467, 49758, 2751, 7268, 102546, 139896, 5441, + 73301, 107048, 22686, 65676, 74336, 38585, 54155, 188892, 31370, 24110, 165124, 94512, 2368, 74483, 9470, 16357, + 111827, 4043, 96403, 170548, 107757, 169476, 28693, 28709, 136231, 117890, 11783, 151383, 142844, 158445, 124615, 108842, + 97138, 165759, 118091, 170718, 96638, 151535, 50875, 26742, 84053, 96653, 22587, 72385, 38691, 81135, 2787, 15019, + 23801, 14048, 175978, 40360, 1588, 63401, 3408, 17858, 10573, 129113, 76021, 1698, 122098, 41563, 91014, 29385, + 77039, 205898, 31782, 66049, 240, 30201, 29388, 36852, 186135, 119600, 62862, 71976, 146101, 66513, 162780, 57466, + 11996, 13198, 214801, 18524, 18143, 9123, 160460, 15278, 112963, 50571, 55357, 30128, 46171, 94183, 55715, 34086, + 155836, 66742, 311911, 22315, 348769, 10015, 161530, 38573, 47795, 32056, 23574, 5351, 121213, 7806, 95295, 21922, + 116266, 57531, 199257, 102740, 132515, 64909, 90793, 50599, 3344, 13994, 153769, 111152, 144804, 6819, 219255, 121782, + 31286, 2640, 54412, 62437, 99187, 80216, 98506, 1973, 255838, 88342, 73727, 16363, 129694, 9582, 119806, 25823, + 35894, 95168, 13820, 13961, 48779, 36969, 6826, 36122, 55235, 97975, 132524, 23098, 148582, 4514, 57373, 77831, + 71967, 9045, 37633, 44748, 65282, 84429, 23687, 7236, 15174, 64021, 45128, 22679, 13920, 80494, 17542, 17632, + 47080, 10822, 58383, 72334, 6147, 18237, 59831, 194844, 108242, 19788, 180510, 100731, 157593, 30326, 279827, 55366, + 125025, 15314, 11547, 48137, 7136, 21272, 48386, 58395, 1286, 9368, 30466, 43535, 28957, 219088, 24784, 30339, + 127956, 154838, 51263, 15865, 28402, 41075, 56222, 63661, 98813, 65963, 4997, 12583, 20805, 75481, 56536, 95023, + 7532, 156833, 60839, 127105, 109417, 17040, 15236, 207517, 40610, 22003, 924, 154828, 5041, 3149, 61584, 15751, + 32958, 30934, 164321, 30734, 15142, 101107, 30660, 103621, 18408, 12233, 98156, 98027, 108596, 11650, 55792, 146477, + 33543, 50057, 68000, 98194, 50517, 22325, 104336, 17124, 27748, 70931, 26858, 118550, 80114, 17779, 47640, 40187, + 233434, 205828, 163803, 9522, 91447, 45870, 85576, 87308, 487, 32686, 244627, 45444, 37094, 10371, 30263, 37708, + 100048, 61011, 174186, 98247, 30541, 198823, 425277, 43101, 43477, 177323, 58960, 83354, 10639, 14794, 48614, 76723, + 89862, 7677, 6456, 663, 155868, 17446, 160748, 3648, 37667, 44426, 160030, 75580, 8726, 48941, 203882, 126698, + 60684, 139753, 22714, 49200, 237903, 165483, 83252, 25239, 73408, 26534, 38895, 18906, 99589, 26437, 80391, 31962, + 12190, 496, 115352, 1660, 38739, 25624, 25196, 314328, 97348, 164824, 64001, 40502, 3914, 11141, 3746, 80143, + 13594, 164955, 149665, 13939, 2680, 66054, 20584, 29040, 149016, 20350, 30753, 3677, 13907, 48796, 31858, 191904, + 30171, 5370, 40086, 3400, 28343, 12830, 135213, 25267, 23530, 168908, 4125, 25811, 115225, 31603, 26072, 36166, + 61104, 83325, 117213, 11935, 35821, 85360, 2192, 24751, 147679, 4560, 76850, 115369, 14337, 33806, 25191, 101567, + 4297, 2681, 88848, 31912, 244282, 37953, 201, 50311, 24085, 34402, 47251, 10729, 326976, 52264, 7804, 24341, + 56428, 30572, 45401, 26493, 7851, 42287, 88129, 15527, 21303, 2927, 54360, 26880, 131620, 27105, 560415, 199310, + 71446, 41999, 39217, 105002, 83253, 83065, 24673, 2975, 68692, 62597, 80790, 14068, 111870, 19347, 24015, 20919, + 5224, 78599, 21870, 186584, 15813, 2869, 29987, 2229, 38197, 107855, 17170, 8632, 49026, 43883, 59246, 115190, + 45057, 33719, 56476, 34016, 13660, 4646, 58901, 85953, 3306, 180651, 187566, 98029, 76345, 77891, 987, 5156, + 40671, 55385, 6206, 26225, 27905, 8848, 109863, 213205, 91072, 48382, 18078, 2792, 996, 28414, 213905, 5931, + 68527, 112270, 20314, 69524, 62085, 20144, 88213, 73577, 91351, 41869, 10074, 4599, 122634, 53272, 97538, 11166, + 55109, 35551, 37066, 49101, 168209, 72102, 82064, 21549, 2024, 9446, 465, 39252, 27560, 116621, 45956, 29498, + 112707, 228850, 6346, 6775, 17352, 61942, 42228, 65657, 8306, 101094, 17714, 124595, 167115, 53195, 69704, 69894, + 169921, 90173, 145431, 10207, 166958, 58767, 73284, 73047, 5031, 13824, 73684, 160378, 46020, 98056, 31999, 14582, + 11844, 8113, 397, 4778, 67284, 141101, 53616, 155244, 19130, 25923, 170625, 42980, 76185, 10190, 22410, 161443, + 6920, 90784, 3737, 114515, 32849, 78448, 37681, 43864, 5450, 79795, 35899, 56126, 14610, 23922, 5420, 21396, + 22451, 72777, 14065, 9126, 21002, 52416, 92088, 1118, 17392, 26827, 105671, 77612, 31872, 56056, 132315, 124658, + 66682, 14163, 4889, 103995, 84796, 16665, 66442, 3877, 13709, 21423, 164865, 200571, 40210, 92532, 13583, 53632, + 59898, 24828, 103097, 34087, 12029, 98561, 693, 181750, 8719, 55652, 72459, 64857, 53564, 30839, 32498, 14036, + 21462, 17046, 22523, 14381, 91884, 124941, 10727, 52730, 21647, 30000, 89146, 127984, 88379, 88232, 34226, 35579, + 194428, 38436, 60128, 81429, 12754, 39305, 70531, 1282, 37797, 8724, 135076, 130745, 132826, 35826, 139941, 21253, + 160016, 52919, 35823, 9597, 22567, 65729, 24463, 1528, 54383, 74937, 66907, 66449, 53186, 30489, 4666, 11722, + 77611, 109886, 5707, 100503, 31107, 178087, 63909, 42663, 8339, 41648, 8005, 13790, 4694, 67663, 118773, 48261, + 19185, 50745, 116520, 7658, 90355, 49068, 180, 69370, 23597, 3425, 108805, 81173, 17222, 9136, 45975, 93798, + 140675, 20433, 37301, 145028, 28442, 25919, 19397, 66698, 23668, 7495, 6626, 1272, 55977, 23033, 63440, 185173, + 9128, 176386, 222575, 17347, 61555, 20002, 35331, 148015, 28774, 22224, 32101, 95270, 155229, 119206, 28922, 125370, + 17658, 61847, 307477, 5300, 9390, 116888, 3866, 54029, 24379, 64983, 6059, 113961, 69357, 18619, 44315, 7700, + 15752, 90871, 72844, 35312, 127381, 68250, 248737, 128932, 239631, 59079, 34253, 144975, 62460, 30364, 34513, 135745, + 93181, 50304, 215982, 80359, 55913, 6568, 31706, 56971, 45133, 253285, 65598, 43884, 140155, 103385, 51233, 124409, + 18676, 15537, 161665, 3137, 25249, 53067, 54148, 94047, 33164, 89524, 31906, 32711, 29581, 75647, 98053, 70783, + 82837, 31002, 153684, 118904, 67223, 110578, 329466, 139791, 22362, 2416, 53013, 8222, 29975, 285682, 15525, 40047, + 7317, 13949, 37038, 37506, 52813, 5084, 19535, 108360, 1943, 92833, 2683, 39660, 29613, 47826, 43960, 34705, + 20012, 41780, 29619, 22730, 207130, 29242, 83810, 88401, 49617, 41631, 13484, 27193, 49651, 48220, 16919, 66661, + 5636, 12779, 78449, 38764, 15334, 19403, 26257, 42121, 36313, 84036, 57343, 3445, 29226, 117117, 1835, 68598, + 28084, 20588, 21723, 125697, 95247, 24456, 23959, 82657, 72072, 191527, 20720, 69714, 19000, 159615, 178013, 30853, + 224932, 9741, 2949, 53392, 15811, 65040, 40817, 91887, 114, 34555, 104656, 35360, 32127, 29705, 5951, 48069, + 38097, 8656, 40672, 7670, 37508, 58545, 33267, 101625, 88507, 8002, 107670, 43585, 37225, 107303, 21839, 889, + 27359, 44829, 8538, 29418, 91626, 112730, 12807, 6084, 12193, 229, 49391, 10635, 69279, 134886, 35206, 15904, + 20608, 57877, 82079, 26098, 11055, 91058, 20546, 8467, 88156, 225150, 50824, 258547, 92808, 13614, 10159, 28825, + 2152, 16714, 38468, 40965, 57259, 46500, 133547, 3619, 55275, 36301, 8395, 62581, 72789, 88910, 30763, 61787, + 24475, 34843, 39477, 127755, 104414, 59623, 65515, 98667, 1708, 34899, 29620, 271962, 9882, 16411, 164317, 42920, + 3818, 33307, 30124, 3990, 48661, 154034, 37142, 8914, 66897, 91285, 266, 18859, 1631, 20187, 18104, 18347, + 34806, 41381, 28001, 36157, 227930, 227438, 205318, 151106, 85815, 19169, 188069, 52621, 87753, 7676, 18539, 52732, + 18321, 344197, 32336, 25286, 250664, 72376, 6599, 95500, 72733, 207728, 65258, 4923, 25107, 38173, 18852, 277661, + 206797, 10280, 68706, 18443, 50891, 10099, 26038, 10350, 8082, 230418, 91468, 2822, 196411, 40589, 33553, 83569, + 92596, 11998, 44307, 17424, 96764, 12849, 152582, 141574, 152823, 25400, 29806, 23815, 65514, 99567, 54295, 22450, + 22819, 8254, 3779, 78344, 387277, 116301, 84235, 146179, 62176, 111891, 75668, 115049, 51225, 54668, 119070, 90975, + 40329, 139109, 42305, 58482, 15563, 96039, 231261, 608, 189, 79423, 116781, 399767, 3659, 221564, 150552, 66005, + 73670, 75682, 162148, 83033, 64357, 106094, 73326, 218725, 51793, 156164, 11491, 97189, 275136, 36754, 71371, 10881, + 33482, 5754, 4091, 21520, 86653, 55930, 27813, 23947, 74615, 44269, 3897, 24643, 67058, 22281, 108426, 85853, + 11318, 7545, 54923, 305706, 125720, 61497, 47223, 139282, 15388, 174552, 34639, 16339, 22388, 14264, 74736, 41059, + 8267, 136640, 7760, 45714, 1730, 8753, 1553, 19445, 102663, 112491, 8628, 51162, 170910, 24623, 47654, 50695, + 40784, 36170, 18125, 20583, 7144, 155439, 2288, 9897, 85373, 25015, 92695, 127810, 13040, 72704, 125096, 5883, + 7088, 2125, 51383, 31073, 94309, 21302, 31925, 6461, 583, 47582, 18007, 56348, 44224, 929, 3859, 152766, + 140424, 28867, 217612, 69471, 77439, 20336, 147072, 7793, 18496, 41996, 9932, 226194, 164026, 97199, 21473, 8913, + 36578, 370703, 232366, 40298, 4722, 21848, 30276, 32613, 9151, 13631, 19868, 1123, 5824, 133353, 58605, 148488, + 63365, 98514, 162970, 68898, 26510, 4366, 150983, 23673, 35199, 75476, 53507, 214063, 40500, 35220, 45220, 83826, + 79277, 94220, 130368, 10107, 32495, 103453, 274743, 4302, 44614, 2713, 157, 20394, 35233, 20579, 45213, 30353, + 9566, 163051, 42976, 78372, 203293, 8456, 26004, 25226, 152144, 18362, 25622, 32068, 7097, 9764, 19804, 105586, + 2330, 79661, 38440, 45558, 183480, 5445, 30623, 15936, 98629, 31188, 21742, 11320, 13423, 50238, 196800, 61702, + 330887, 6907, 165378, 29192, 44130, 36247, 125815, 76135, 68600, 3478, 102912, 38948, 30939, 32718, 115597, 14759, + 97829, 178650, 6398, 96989, 31012, 80346, 29170, 167665, 345465, 32639, 105679, 182158, 56747, 62826, 5573, 31888, + 94879, 251468, 82742, 74122, 4939, 105988, 50230, 43427, 160027, 68618, 115561, 16419, 149761, 8457, 34061, 57375, + 99423, 60632, 123694, 18569, 46099, 3392, 96711, 126188, 32251, 264812, 49338, 132633, 15332, 90783, 67149, 64854, + 8761, 62393, 76418, 81682, 96094, 50655, 103271, 39486, 128555, 50988, 205607, 4238, 5763, 202341, 1257, 225585, + 235318, 53879, 36434, 4671, 222223, 22096, 143525, 96909, 55729, 20281, 64025, 133074, 30241, 79441, 19012, 18415, + 55420, 135097, 144146, 48331, 201746, 72776, 317545, 66575, 3987, 123583, 63087, 50450, 81382, 173, 71558, 23663, + 8259, 7334, 32703, 83813, 49325, 14244, 94476, 64667, 23287, 26116, 23310, 52112, 7719, 44701, 18954, 2509, + 24633, 12226, 87574, 88717, 251100, 14491, 4274, 63011, 92311, 24796, 10214, 29179, 18591, 35862, 3752, 140316, + 110533, 16614, 111729, 140400, 64759, 105653, 88014, 99594, 170260, 100542, 4164, 21095, 393860, 1570, 11733, 2292, + 7175, 53291, 7019, 12084, 21144, 53309, 55975, 40606, 132887, 54500, 21593, 18556, 3593, 69, 28773, 8032, + 75346, 68243, 36013, 38871, 101277, 105666, 51660, 13332, 91109, 139335, 73372, 29329, 16387, 148050, 129561, 74071, + 259187, 18882, 26921, 62561, 11627, 44409, 155193, 111945, 57459, 200411, 28094, 21626, 173829, 42067, 142855, 22469, + 44694, 41492, 188424, 1317, 55780, 178410, 31665, 32476, 49797, 50096, 133424, 54045, 89192, 278490, 20313, 169877, + 120443, 901, 6058, 24102, 62622, 149284, 65967, 57346, 3904, 11937, 41372, 17994, 14814, 66911, 235601, 80170, + 23887, 4818, 145255, 128004, 1027, 25782, 724, 207408, 112258, 39194, 29870, 74011, 64955, 219934, 119689, 118077, + 99800, 207128, 83883, 66658, 11132, 62891, 83085, 25216, 66353, 9815, 11198, 16652, 36202, 13812, 128388, 227653, + 48624, 40080, 23190, 29953, 18158, 102077, 62237, 3854, 88481, 9533, 51501, 68427, 96882, 27608, 35856, 58154, + 43059, 186515, 64641, 36774, 11040, 20138, 41376, 37623, 2455, 47929, 25769, 20233, 7077, 19150, 180387, 26425, + 21651, 71540, 74198, 49630, 181159, 39337, 47603, 10453, 99126, 357301, 22413, 127961, 100434, 22332, 3856, 103557, + 61388, 149292, 137919, 5062, 68186, 8654, 8282, 23829, 19161, 9891, 41737, 115440, 110468, 4212, 22815, 35452, + 120052, 148515, 40823, 61325, 8819, 4354, 51789, 12976, 55324, 24368, 77300, 8725, 41494, 7736, 73140, 57163, + 7014, 42056, 13817, 48686, 37689, 9578, 13571, 104584, 3618, 3981, 2615, 22449, 87729, 68084, 20425, 146810, + 92615, 53907, 63016, 17337, 207943, 48593, 10105, 121091, 9823, 47400, 75309, 56784, 128657, 106634, 43561, 70981, + 12587, 51472, 92150, 54178, 15857, 121204, 6055, 42142, 6379, 144024, 8486, 145642, 67628, 94025, 8294, 117913, + 51216, 150314, 153347, 1042, 71775, 2265, 3224, 68227, 157617, 10548, 144881, 33324, 63244, 38654, 28271, 8525, + 1503, 958, 5778, 53723, 3864, 58302, 39847, 53240, 218754, 39512, 6285, 19507, 110864, 63755, 37263, 11172, + 5769, 8998, 16662, 20146, 21492, 50772, 13346, 118643, 7243, 7129, 118273, 175534, 1192, 22122, 76882, 43114, + 87921, 153196, 45106, 138465, 85529, 3075, 30600, 13487, 31512, 11916, 22411, 68282, 18537, 64660, 19916, 149911, + 42698, 4415, 22126, 52393, 35350, 288666, 27537, 97849, 73510, 52941, 15929, 294, 84474, 45720, 39658, 4675, + 34301, 453, 57231, 29931, 8991, 192137, 112683, 33353, 21013, 82429, 32028, 9768, 35034, 2225, 27949, 96606, + 566, 33483, 30858, 67296, 38832, 15545, 58085, 23327, 19838, 45987, 38289, 25368, 35586, 107553, 62141, 38448, + 37216, 107939, 32602, 87203, 39413, 653, 178536, 85622, 55006, 18371, 38085, 25177, 12178, 39794, 54315, 242178, + 59742, 149144, 10766, 127632, 856, 10442, 37459, 12213, 84563, 16802, 59641, 26529, 6900, 63962, 37381, 10736, + 25632, 95267, 29099, 16636, 17437, 23052, 153874, 11429, 49786, 45113, 58163, 65817, 30723, 19440, 8273, 176150, + 13847, 87151, 4967, 13744, 70845, 25959, 93062, 22114, 4044, 73295, 41922, 20005, 7843, 56037, 104704, 125857, + 23944, 178063, 113180, 671, 235976, 152556, 97222, 136235, 55530, 17906, 85124, 11074, 48942, 10651, 199, 27527, + 6518, 91832, 29772, 61127, 20939, 1458, 24121, 44597, 73769, 4219, 49564, 15715, 192653, 166296, 109313, 43836, + 52936, 33537, 1148, 25047, 95207, 270590, 56882, 97809, 23895, 31177, 26762, 3184, 132542, 49663, 59748, 3068, + 142982, 88469, 25870, 43552, 22632, 51664, 23936, 32111, 87452, 107086, 104873, 11521, 48042, 93260, 56685, 20765, + 54018, 59944, 11341, 144539, 178468, 24105, 32314, 35295, 10728, 11236, 29477, 35284, 26230, 226, 165790, 42461, + 23559, 117007, 118411, 290079, 363, 78533, 53121, 31307, 81269, 1905, 16159, 14155, 142012, 33360, 14799, 52790, + 5718, 161126, 136815, 27111, 346258, 23627, 343, 945, 31456, 9607, 277463, 25540, 84333, 314287, 2083, 110065, + 246476, 16799, 92713, 55883, 51018, 19760, 17707, 24992, 66692, 259273, 23704, 9218, 101804, 59562, 16967, 86612, + 120570, 10762, 23969, 43849, 39962, 166523, 272460, 8995, 30373, 16246, 141683, 23812, 70593, 83230, 21943, 323413, + 2864, 2355, 100471, 19429, 60541, 82374, 42741, 35603, 19425, 290629, 43248, 33804, 54209, 7864, 21420, 71753, + 104092, 2325, 5902, 24070, 7201, 62951, 120053, 14462, 31545, 99914, 13564, 113310, 48018, 103024, 129706, 25209, + 25865, 23609, 5945, 17372, 15442, 45351, 49273, 3849, 46257, 44296, 17650, 6, 40443, 52796, 50158, 89987, + 8328, 144410, 81530, 54477, 6451, 36594, 86466, 8893, 111782, 198927, 159705, 4360, 47527, 36893, 117496, 37320, + 97754, 50697, 10204, 162372, 33046, 29631, 95221, 7160, 470, 118627, 17716, 97731, 245116, 237362, 49912, 10078, + 31095, 22842, 98784, 60332, 39, 19235, 8973, 120765, 91934, 111045, 58972, 128887, 87208, 67602, 46022, 29104, + 73470, 5539, 281467, 182667, 36708, 176270, 59432, 31026, 36521, 1009, 33426, 52275, 12801, 73675, 38019, 38883, + 70624, 9854, 44370, 2164, 36272, 3412, 81053, 50116, 8892, 86510, 11513, 257297, 79768, 14274, 40531, 33581, + 12427, 106809, 39327, 15343, 55454, 3699, 118114, 30358, 103756, 3575, 184159, 130664, 5908, 82458, 120759, 58923, + 52390, 273629, 62193, 75485, 62962, 34707, 223853, 87062, 22720, 170728, 85085, 23937, 141138, 3062, 19956, 33198, + 94634, 43295, 14025, 57761, 41689, 20778, 135679, 19997, 128402, 22478, 95668, 1172, 126390, 47383, 9006, 96799, + 6628, 94152, 175749, 12095, 106394, 149169, 112174, 23774, 35527, 95733, 7587, 79649, 134394, 37602, 25349, 151895, + 82727, 41687, 20559, 85846, 254651, 19160, 594, 147220, 194502, 40954, 17422, 66352, 148064, 13518, 9302, 36919, + 89549, 90619, 13212, 61499, 3202, 41426, 127530, 57213, 28359, 266732, 167478, 35161, 957, 34000, 1394, 74891, + 21954, 67533, 109988, 70343, 27906, 20087, 74943, 12177, 49840, 84890, 252007, 38133, 142747, 13270, 9011, 1956, + 8307, 41129, 241047, 11967, 24206, 16598, 65926, 114801, 48978, 39444, 29569, 37783, 1186, 28611, 15878, 63609, + 71728, 79096, 37857, 7905, 133038, 64595, 170871, 4983, 71474, 90251, 41794, 1133, 91306, 2347, 11977, 301891, + 6333, 120429, 31567, 114536, 110959, 92026, 77401, 131107, 74600, 4947, 14258, 12464, 70387, 792, 46742, 49365, + 18983, 55855, 53296, 26567, 62609, 186547, 111481, 9625, 56057, 154515, 28578, 317259, 22970, 130016, 87061, 200872, + 1147, 156105, 94296, 61010, 135850, 23757, 304, 68146, 1321, 101784, 28699, 41754, 28834, 26821, 22763, 34257, + 3578, 58871, 46251, 42855, 59715, 38961, 106435, 68266, 102227, 100604, 165318, 20000, 32827, 79754, 41071, 488644, + 81415, 27823, 114001, 14947, 99952, 20692, 48050, 23304, 55636, 16428, 9523, 58498, 257598, 22601, 34441, 124581, + 390, 131974, 174602, 248497, 22702, 5303, 93016, 7999, 35701, 16482, 87643, 40024, 85872, 13254, 27598, 5591, + 402916, 332853, 161899, 167074, 39216, 53652, 3842, 15164, 189795, 61422, 24281, 156829, 14929, 126509, 20650, 40168, + 19467, 70654, 8316, 5943, 10112, 63120, 121775, 4962, 144422, 66650, 69732, 29954, 61514, 18947, 174426, 19368, + 5279, 6294, 146465, 101384, 63421, 46353, 14826, 52334, 134686, 24672, 129081, 113192, 41436, 46150, 14514, 94104, + 8424, 94187, 87766, 25900, 51925, 128539, 30970, 101548, 10598, 4499, 46545, 283177, 132295, 66488, 16337, 205974, + 124416, 16473, 174653, 64458, 4604, 27366, 56141, 24652, 194739, 122988, 146663, 6230, 210929, 11416, 14833, 244818, + 57866, 80769, 9608, 46573, 31829, 126854, 41657, 44969, 51626, 23726, 29384, 7856, 50007, 53704, 180935, 69829, + 9976, 25004, 73613, 77676, 91878, 46340, 96749, 1491, 61906, 71825, 107515, 42399, 56168, 45795, 84470, 28713, + 81906, 14060, 29478, 12282, 60918, 152243, 2069, 14131, 61859, 62748, 45514, 41973, 40017, 51530, 81012, 35281, + 23059, 3091, 91182, 40986, 16887, 215039, 49110, 48902, 40927, 4664, 22408, 2186, 62064, 38288, 20072, 39739, + 12785, 46679, 137755, 83190, 32893, 3331, 21910, 59337, 32913, 69540, 19401, 3027, 21782, 89291, 9283, 160441, + 93965, 2532, 14763, 2928, 20169, 41863, 117119, 29296, 44387, 196228, 25734, 37480, 79084, 56135, 111008, 23529, + 38463, 12553, 65044, 45442, 11457, 157661, 136804, 196290, 93950, 78976, 90672, 49250, 27127, 21222, 172616, 123647, + 157050, 84415, 40826, 176737, 2697, 48083, 49157, 116229, 337088, 109380, 128213, 191771, 5116, 6808, 21572, 57279, + 54128, 114903, 1829, 16332, 48255, 41551, 56053, 69223, 33279, 27772, 38971, 152878, 8821, 42333, 270100, 30247, + 27352, 31219, 62315, 48036, 25515, 5676, 64424, 6842, 124022, 67108, 75224, 26671, 65710, 5485, 81101, 180131, + 28906, 35401, 41128, 7420, 38557, 103577, 27651, 76992, 33390, 277265, 119976, 16075, 1722, 4016, 8160, 16260, + 104435, 28594, 108521, 42619, 72215, 13679, 41467, 25078, 38551, 17674, 12470, 98147, 12094, 238313, 103012, 94925, + 30978, 17555, 9120, 57009, 25113, 302349, 310035, 87798, 6671, 70215, 168771, 51466, 37355, 11995, 27263, 18145, + 175109, 36992, 46153, 119925, 42862, 41147, 72904, 29988, 98024, 78321, 120886, 98773, 65406, 84549, 46294, 207054, + 221276, 28987, 23494, 87283, 59624, 116795, 91279, 21644, 118012, 30256, 26515, 54289, 64637, 18879, 2366, 48426, + 78760, 113223, 59165, 22607, 86697, 2006, 87700, 5586, 21426, 221804, 43982, 42892, 1639, 18263, 97022, 7861, + 40350, 35831, 74589, 15387, 12584, 16272, 2350, 34840, 67193, 166860, 62, 129059, 84144, 13945, 8872, 16116, + 31396, 12848, 46665, 69587, 7863, 337732, 2021, 215090, 143011, 19774, 108067, 9097, 69629, 62086, 41087, 118595, + 63112, 27350, 127724, 168612, 9454, 94824, 47240, 23536, 28666, 13015, 8075, 83300, 65798, 136675, 46387, 66592, + 46372, 22200, 69177, 100328, 134721, 10046, 40569, 11636, 6314, 82208, 7888, 75928, 51402, 4086, 55946, 3983, + 30837, 6098, 23497, 57256, 151922, 183027, 50022, 109927, 2847, 71736, 48147, 21198, 38676, 17208, 38494, 26901, + 38008, 40243, 23664, 42890, 92823, 28712, 10052, 19450, 136245, 1296, 26037, 115524, 17540, 81959, 87998, 109947, + 5504, 135131, 70356, 151584, 109295, 23619, 23856, 16295, 205242, 19398, 32059, 269066, 37606, 78075, 65249, 115008, + 5211, 52062, 83105, 34423, 214892, 56403, 19696, 105923, 1586, 3982, 120399, 20581, 20670, 68532, 88135, 29281, + 208711, 27405, 153585, 40, 137743, 9722, 8280, 31119, 19328, 72088, 60804, 21766, 40652, 52930, 36086, 14049, + 16995, 33305, 97171, 70003, 20023, 20591, 134059, 31794, 14657, 45601, 41289, 83347, 154919, 205038, 18514, 16315, + 34422, 97287, 94105, 23527, 12996, 13917, 114122, 23417, 13918, 46358, 21310, 7972, 38221, 68113, 107783, 16386, + 47690, 56795, 8499, 18545, 16398, 691, 133344, 34814, 2959, 20119, 119395, 59372, 37680, 79653, 29760, 7411, + 89122, 12450, 10343, 56156, 6721, 9880, 13905, 42800, 198469, 177097, 106548, 82488, 91876, 24609, 10706, 76004, + 172043, 23144, 14890, 62366, 83898, 12968, 38955, 44234, 101992, 9519, 85215, 71444, 26084, 100199, 129339, 314155, + 94570, 5416, 9451, 23094, 3635, 8260, 19505, 102704, 76958, 35234, 26525, 2551, 22853, 44177, 13568, 15372, + 76497, 42940, 8996, 82568, 38266, 73994, 25359, 100653, 176590, 44214, 60988, 126450, 168403, 88749, 20773, 72958, + 44464, 16442, 108241, 43912, 142840, 28715, 26132, 62091, 79180, 335, 13854, 117482, 184594, 27524, 213171, 29588, + 1984, 151855, 9907, 24196, 41806, 10248, 139515, 110653, 83147, 67906, 50005, 126920, 11985, 7146, 70966, 73392, + 6546, 11571, 36898, 51082, 366068, 14298, 110969, 8480, 59732, 41291, 11948, 60798, 24533, 35525, 46894, 151863, + 271505, 8978, 152334, 240030, 8736, 86988, 55120, 56449, 39084, 131143, 52149, 20472, 222992, 92098, 113905, 160942, + 93429, 46821, 134338, 13359, 28962, 133572, 63437, 17720, 58985, 12668, 50951, 30088, 86665, 12598, 38135, 100049, + 8432, 23582, 12520, 74142, 30028, 37676, 72636, 19758, 14548, 75515, 10553, 14518, 32439, 41532, 19021, 32738, + 54424, 77334, 95984, 30113, 165029, 21959, 45058, 51094, 55175, 7821, 3808, 143697, 27458, 92493, 25546, 44547, + 69046, 4809, 22393, 105573, 121277, 83916, 93708, 65917, 46168, 169086, 41887, 133671, 33732, 21296, 85711, 93384, + 20661, 16850, 126144, 79900, 24581, 92813, 68853, 7556, 135574, 40156, 253, 27866, 123110, 108105, 8863, 6885, + 37556, 40974, 5474, 42318, 79260, 53275, 55822, 124845, 72611, 23898, 88911, 72971, 16957, 97387, 53099, 8161, + 12939, 7235, 19325, 37236, 46162, 6870, 103036, 9217, 58238, 52894, 25497, 87733, 44907, 108667, 102200, 20343, + 72936, 85840, 55962, 34676, 253758, 69668, 96389, 111954, 41324, 167841, 8806, 158362, 32518, 41375, 120632, 73135, + 96480, 21314, 4720, 259790, 11949, 5752, 116141, 21106, 124438, 7908, 114832, 63475, 65280, 49770, 31147, 141323, + 43256, 220098, 15804, 42500, 34107, 104757, 75473, 1964, 53533, 71047, 83715, 42845, 43531, 77188, 2321, 57356, + 37037, 106618, 27233, 20062, 28366, 40192, 37908, 345556, 45970, 11045, 55249, 98674, 32741, 77466, 25566, 44398, + 173438, 69888, 65582, 3754, 6121, 34129, 9737, 95439, 194202, 164322, 2102, 4704, 62969, 40994, 31759, 21994, + 26355, 77991, 29831, 64006, 30314, 111389, 59636, 131909, 58370, 93916, 82377, 99217, 28455, 53729, 81544, 46068, + 1848, 132138, 3234, 103227, 50519, 863, 34429, 22675, 82830, 85513, 87414, 12210, 90393, 3294, 143767, 94310, + 21761, 100363, 279561, 80941, 295490, 75238, 72247, 3049, 10936, 2278, 38484, 33144, 256940, 101817, 33856, 75506, + 133568, 39527, 156016, 262504, 44050, 98168, 25682, 41968, 20269, 88567, 37803, 5755, 4089, 36260, 33454, 69130, + 27457, 129154, 52277, 26140, 21610, 15320, 67142, 111206, 219460, 45685, 70880, 105780, 36743, 123247, 83438, 52208, + 14821, 80935, 29659, 180718, 101388, 45817, 3374, 57612, 52005, 6448, 72882, 184891, 13124, 12704, 30703, 25929, + 30979, 69399, 15380, 74068, 140816, 117172, 5753, 5065, 167362, 3697, 123452, 43307, 26054, 54528, 105177, 21154, + 18458, 72597, 72966, 61322, 60789, 5516, 142428, 84303, 34917, 14578, 112502, 12928, 40447, 8085, 94588, 23066, + 26606, 8666, 27561, 98346, 33422, 14547, 68915, 108761, 9066, 161082, 19796, 27553, 3452, 27494, 15534, 23906, + 83614, 19, 132782, 13705, 5761, 29605, 59087, 120796, 20263, 12290, 12851, 50370, 137238, 39286, 94500, 20667, + 25038, 21628, 33990, 8981, 91310, 200127, 63967, 88268, 101, 286303, 114772, 71059, 52322, 129656, 47566, 12562, + 74548, 64285, 49207, 24655, 42572, 28607, 237181, 77595, 38084, 16719, 839, 81694, 214054, 17721, 4580, 98901, + 186568, 121462, 138040, 42832, 31802, 161998, 197148, 18980, 17665, 152652, 24115, 15160, 30620, 99343, 9889, 87020, + 141936, 7344, 42231, 98797, 37730, 129285, 120827, 160915, 14420, 2535, 155000, 34611, 4265, 102563, 57689, 91604, + 187218, 17667, 1262, 39897, 49640, 200660, 37670, 19608, 188208, 32890, 127419, 68124, 51441, 81359, 123584, 58473, + 55388, 45140, 15680, 85159, 96452, 16120, 30294, 94559, 66659, 44469, 59032, 36146, 40869, 33455, 63569, 7922, + 42039, 12261, 115220, 18301, 60967, 146149, 29288, 13403, 221027, 9100, 52523, 139159, 19234, 40843, 206228, 37834, + 178581, 17027, 40896, 45534, 29105, 2544, 91128, 79269, 96050, 47912, 285426, 27282, 9165, 151, 46166, 7045, + 196118, 46867, 66112, 143251, 157335, 55563, 96710, 8207, 3738, 72403, 61089, 157979, 40354, 156883, 40650, 178616, + 117436, 102312, 20179, 41865, 2965, 21966, 28441, 168958, 34136, 38297, 9787, 20997, 59659, 36744, 60366, 114471, + 15570, 25271, 29375, 258002, 50843, 37278, 68479, 43715, 230035, 5024, 2970, 91666, 31444, 117750, 16847, 112904, + 71260, 12363, 55600, 48521, 43886, 46675, 13532, 57721, 18316, 20619, 56356, 4628, 5387, 78044, 11, 33577, + 38500, 104616, 16018, 21088, 168508, 6053, 33669, 9683, 17406, 34961, 60264, 30468, 32174, 24071, 78408, 25436, + 8828, 39141, 39373, 37714, 103373, 109235, 33475, 22349, 143806, 196511, 45872, 33957, 90367, 15839, 11624, 84305, + 3560, 26149, 26619, 50807, 18719, 36474, 43129, 70576, 122310, 12311, 103752, 19102, 16508, 25987, 44388, 63236, + 26719, 131892, 134604, 63602, 2541, 237340, 98802, 45539, 105429, 117394, 38028, 181798, 6645, 86186, 32830, 114213, + 37998, 21792, 4332, 69153, 73190, 173015, 127914, 9801, 10591, 4848, 14686, 23714, 235916, 53231, 142998, 35580, + 49737, 63998, 15639, 52103, 87112, 228066, 40885, 51065, 233941, 23415, 4046, 91775, 53188, 141652, 6728, 119152, + 32193, 121081, 48378, 63940, 79154, 100136, 68457, 56982, 4544, 133512, 181089, 126816, 52905, 78546, 37550, 11149, + 126477, 134049, 124361, 126465, 7580, 1593, 86300, 14758, 63501, 80748, 18813, 163792, 57314, 22986, 6141, 11406, + 3216, 32494, 110332, 28452, 31337, 162136, 21024, 26738, 6541, 7188, 22430, 92369, 103083, 128099, 204997, 51873, + 60846, 69568, 20383, 4773, 49, 48196, 29395, 14748, 9756, 43703, 50109, 163732, 15481, 130284, 88137, 10991, + 1355, 101243, 3715, 54949, 43840, 19376, 118637, 43614, 14319, 50237, 47535, 50675, 13743, 107035, 43658, 76751, + 27486, 7709, 13643, 96042, 10222, 21204, 124418, 113169, 73114, 71157, 117166, 121290, 230718, 203167, 36470, 64894, + 418644, 66714, 116018, 28732, 16706, 52824, 36929, 51413, 6674, 128215, 137726, 19964, 279748, 26329, 87214, 10512, + 23058, 116855, 58012, 13318, 45273, 76517, 33779, 74266, 295831, 1336, 47046, 106101, 86306, 13, 79499, 18377, + 2743, 123188, 92968, 51882, 5535, 6799, 86683, 1562, 88773, 138107, 96667, 11031, 21829, 4831, 30926, 76005, + 35253, 174880, 61764, 219165, 120938, 41317, 25064, 15262, 31153, 216734, 168694, 126329, 3169, 7494, 80595, 54300, + 16839, 27391, 60115, 17288, 42847, 68970, 16373, 61298, 8751, 32529, 67945, 20542, 80974, 71952, 139421, 42317, + 33942, 21817, 38951, 12239, 36867, 67209, 48716, 1479, 35514, 105236, 52386, 100232, 16485, 31451, 46256, 39894, + 26474, 60702, 20094, 10295, 77775, 1748, 117588, 48134, 56877, 51915, 11560, 224153, 5391, 3701, 26137, 58941, + 48346, 80382, 15181, 131665, 3882, 7358, 42702, 14975, 108713, 6343, 80451, 72290, 31403, 4314, 32244, 5000, + 27804, 42280, 29989, 44929, 55248, 6075, 42803, 20470, 26235, 53651, 44765, 109461, 43821, 69384, 24056, 170949, + 136104, 115740, 191734, 36140, 40118, 138988, 7264, 71698, 175507, 12620, 155241, 7468, 28034, 185481, 2642, 23421, + 203908, 130353, 51032, 89780, 18732, 56166, 21485, 47669, 1788, 12936, 94197, 57925, 34030, 152347, 132787, 4308, + 106427, 53840, 90424, 21211, 36958, 111950, 33872, 29233, 54359, 51008, 62593, 42396, 7251, 123282, 198715, 53630, + 44936, 14249, 77826, 258614, 15356, 45209, 51025, 80569, 69139, 10507, 10673, 56339, 455, 43204, 39237, 14263, + 157915, 72576, 100737, 58151, 22173, 51683, 197114, 28812, 140291, 15706, 153760, 89962, 50348, 37718, 108861, 24876, + 43275, 55149, 227847, 109122, 82066, 68840, 83530, 39884, 49621, 18581, 228664, 30516, 54952, 16372, 8186, 29493, + 15216, 129920, 30573, 45435, 36226, 109626, 22456, 7678, 96695, 109013, 34730, 57022, 66855, 100447, 13026, 32965, + 6936, 272617, 48390, 49584, 1987, 61711, 3050, 110021, 8227, 8769, 96593, 27031, 196087, 22068, 19421, 11454, + 4631, 39978, 242671, 88040, 68827, 46976, 54523, 87226, 99004, 37956, 109647, 20830, 47541, 918, 286374, 22872, + 110265, 74675, 4723, 101851, 17953, 216583, 119919, 65609, 147605, 1909, 30855, 55924, 110242, 97095, 129683, 9370, + 58520, 14221, 15934, 60347, 31312, 158125, 25989, 55743, 38724, 14896, 158699, 35678, 329975, 16243, 72941, 112881, + 642, 11436, 38291, 112105, 3155, 67914, 72572, 24476, 34497, 104985, 29314, 84672, 75937, 22802, 6429, 75538, + 6207, 163504, 199842, 23456, 73843, 79289, 235636, 7573, 6120, 124975, 52978, 95704, 17249, 175679, 9569, 104975, + 51429, 329147, 13133, 168135, 117746, 78571, 8080, 58294, 3218, 58849, 10361, 62017, 910, 33143, 555, 31814, + 68961, 145868, 122784, 15453, 319671, 26077, 236592, 74939, 14938, 125306, 95117, 36290, 29555, 75543, 1445, 3405, + 34700, 143718, 17140, 139460, 1649, 866, 29186, 90440, 66673, 82069, 69865, 13592, 72268, 105954, 55710, 1985, + 9655, 105536, 52207, 38806, 76800, 9513, 81900, 12656, 153087, 6755, 43141, 21061, 6941, 16813, 142735, 31152, + 210168, 41188, 100125, 29783, 27130, 37384, 93862, 71525, 35398, 12398, 26982, 303589, 1780, 9910, 34746, 13281, + 73242, 165400, 2032, 112057, 3135, 21087, 114080, 35562, 56689, 79328, 57005, 56383, 19556, 8454, 47363, 3132, + 165307, 54577, 13611, 2516, 8765, 81373, 65366, 57451, 35967, 256887, 71960, 42183, 121458, 34333, 89472, 15018, + 13333, 75535, 131910, 13497, 70453, 25831, 37776, 56546, 17350, 31676, 97161, 28621, 117253, 29257, 16050, 488, + 22265, 48754, 128425, 182002, 13340, 90943, 70744, 33837, 44265, 92131, 45994, 1482, 39869, 27394, 57718, 13956, + 441, 5487, 70995, 55529, 3742, 61617, 118889, 92005, 135025, 86222, 66571, 254704, 23581, 37646, 38231, 4846, + 33309, 92236, 6134, 18657, 16543, 2124, 77619, 45274, 17731, 39418, 106157, 55973, 13291, 9746, 65544, 32616, + 157637, 33791, 108671, 24882, 283005, 65971, 70349, 6146, 21408, 15989, 88659, 365, 101360, 128119, 22909, 2662, + 63887, 10345, 23354, 71146, 52312, 30052, 45711, 99568, 83873, 506963, 2554, 13515, 5338, 289237, 32776, 59272, + 233779, 82419, 26344, 71757, 23759, 15290, 14476, 154914, 186949, 43876, 60677, 44353, 34531, 1802, 77130, 15863, + 177320, 67156, 10629, 89062, 38069, 81953, 13367, 22945, 156465, 63862, 319916, 37995, 91004, 119548, 320016, 44035, + 19353, 17852, 114239, 24818, 59852, 114160, 51645, 12833, 68160, 65930, 36636, 297383, 14891, 2853, 73690, 26169, + 1338, 1151, 23088, 98141, 1072, 29833, 31924, 58527, 29823, 49223, 13440, 94333, 1950, 42392, 24753, 253517, + 28901, 4590, 1846, 35647, 81407, 166926, 16913, 15179, 313445, 157404, 11324, 72420, 73038, 62980, 67242, 98614, + 84807, 1288, 18715, 35345, 162348, 49709, 92384, 35688, 240257, 105106, 97424, 336014, 37162, 31857, 24409, 137966, + 138934, 29778, 20791, 88000, 16111, 4596, 20363, 25650, 58013, 116702, 57820, 5668, 41253, 31169, 251377, 5377, + 102951, 209713, 150400, 82980, 16457, 95615, 28341, 34104, 96056, 14290, 87446, 35563, 19541, 9842, 4673, 34998, + 56402, 37494, 140987, 144287, 67217, 38738, 69684, 90560, 41638, 6371, 67553, 130177, 94381, 18943, 22304, 5556, + 89674, 1540, 104069, 26091, 29481, 79348, 127520, 1738, 37456, 64314, 202862, 31135, 80815, 1319, 24743, 103040, + 151579, 2886, 45671, 10020, 13937, 23292, 25393, 58266, 13683, 1161, 48094, 156120, 132537, 22049, 1682, 76886, + 19699, 10385, 1058, 254528, 134545, 55004, 82397, 41659, 67020, 195747, 38970, 40798, 29816, 202866, 95435, 5414, + 222341, 122682, 143053, 294222, 141235, 18722, 36979, 52903, 427578, 91648, 78453, 23736, 48868, 95317, 62318, 53265, + 129557, 41511, 51444, 17062, 233342, 157429, 4110, 25190, 23077, 32496, 234890, 104393, 87871, 119604, 172405, 31296, + 16213, 41034, 147157, 76, 18728, 337132, 33035, 74049, 16184, 117337, 1430, 50125, 9469, 13116, 96853, 186079, + 37913, 79652, 76574, 12079, 19680, 20090, 74134, 22992, 2798, 111900, 29035, 78700, 171356, 103866, 25861, 76971, + 178328, 160559, 131679, 44747, 13216, 89528, 50885, 79922, 50049, 78775, 61642, 115486, 72690, 15613, 40111, 8974, + 71904, 99272, 76547, 36995, 124644, 58876, 62375, 56306, 55455, 10949, 9333, 20277, 7504, 41873, 102574, 28557, + 29052, 18656, 226780, 29795, 41036, 52032, 84065, 51914, 266546, 6019, 73011, 15118, 19899, 148821, 109409, 68842, + 30391, 102037, 158644, 78906, 188755, 20593, 56915, 26262, 8659, 76359, 39099, 42863, 59469, 24343, 170097, 72940, + 16, 31679, 33449, 44831, 104298, 168570, 329243, 12874, 112943, 10737, 81733, 10145, 53865, 30398, 84862, 90377, + 76203, 66, 37651, 15508, 138226, 36312, 36084, 66979, 68857, 69503, 23486, 27392, 139953, 43251, 74333, 109079, + 14125, 42935, 117495, 77115, 107625, 70706, 2266, 28248, 119795, 6372, 79378, 83196, 173133, 134246, 42289, 4799, + 4398, 34848, 30176, 134351, 50273, 66466, 87051, 132965, 48808, 31554, 48150, 75235, 54390, 30193, 11461, 79397, + 16466, 17661, 7427, 28480, 122086, 37993, 13959, 83801, 31835, 33998, 164771, 60458, 67035, 180999, 5256, 7006, + 50971, 114665, 163017, 23336, 48859, 250788, 4340, 9613, 7508, 81510, 27383, 68480, 46427, 15448, 81334, 97899, + 66477, 71, 139832, 7506, 73021, 97330, 136340, 46842, 84615, 21471, 141895, 32003, 39985, 62480, 13666, 12717, + 83076, 96305, 46422, 172149, 46779, 38567, 66589, 155205, 201569, 190175, 4693, 89306, 53336, 145244, 26386, 78125, + 36443, 60742, 64991, 315, 60865, 22001, 34462, 3145, 168164, 7227, 45365, 52278, 143810, 15529, 219120, 21490, + 51393, 22637, 26823, 15222, 25548, 102002, 40489, 96757, 169307, 3643, 119915, 68728, 32896, 113685, 70503, 18482, + 24485, 209111, 5537, 41079, 38424, 97942, 125499, 59570, 21837, 20492, 31623, 9701, 29087, 11628, 19585, 9000, + 275813, 117347, 75561, 10000, 51674, 25141, 80217, 10734, 6714, 202302, 17083, 85695, 64883, 71665, 13202, 80751, + 46169, 222686, 49498, 67783, 187369, 37577, 13134, 27844, 55186, 70471, 20101, 307605, 76192, 4390, 46641, 16931, + 12852, 7169, 5321, 137488, 12018, 197662, 2595, 12702, 62134, 52236, 43904, 2706, 31067, 311914, 70629, 280345, + 118303, 59493, 9152, 296895, 16542, 4127, 190174, 11204, 12125, 33624, 43704, 629, 10579, 161171, 436098, 110011, + 4928, 20741, 120332, 41283, 26291, 13782, 65933, 147206, 43854, 143015, 24103, 185039, 7091, 135245, 92175, 293076, + 10946, 19925, 19967, 110847, 253716, 42758, 95038, 69599, 109062, 27063, 120815, 57458, 39283, 10218, 39354, 7499, + 17261, 8263, 7839, 189220, 113012, 110601, 48485, 156100, 258512, 41840, 167472, 67791, 47764, 14675, 53087, 6354, + 125126, 12700, 41054, 45096, 32646, 70686, 11736, 1417, 55892, 49536, 45376, 6942, 80279, 12070, 89681, 183322, + 201623, 35389, 58180, 430, 149872, 18459, 444892, 19950, 3192, 82244, 305001, 83495, 385, 1258, 82408, 33652, + 1208, 738, 12995, 21781, 48750, 13634, 68571, 68149, 5376, 30653, 64669, 33991, 58738, 87302, 80018, 88747, + 22335, 35680, 106650, 40779, 5427, 30033, 3552, 51590, 82416, 25102, 25208, 3949, 47811, 74006, 93322, 124119, + 32435, 357395, 49716, 13835, 143086, 4083, 79989, 41030, 38930, 21275, 146867, 20485, 94128, 11151, 10472, 53127, + 59975, 30973, 116792, 75634, 156037, 15565, 112131, 58155, 37977, 33863, 74566, 194491, 38224, 22622, 88291, 51351, + 62485, 19885, 25695, 49858, 7698, 124574, 37501, 200, 50405, 11713, 287549, 195058, 71027, 14971, 39645, 70772, + 16462, 27850, 51933, 19178, 21559, 27321, 3458, 43074, 136153, 7003, 195280, 149565, 34131, 52040, 1210, 6796, + 107506, 11880, 278327, 23579, 162069, 86206, 10271, 126827, 63703, 27398, 13524, 13255, 3101, 29045, 7198, 55423, + 215029, 1232, 15504, 168293, 40407, 14532, 80445, 19258, 4178, 203513, 68565, 70756, 3774, 260344, 5233, 163405, + 9187, 46762, 107090, 26759, 80019, 11197, 524211, 114351, 17880, 91874, 35307, 46472, 97926, 12980, 2932, 75, + 67579, 57528, 43925, 163283, 2600, 68602, 18775, 154886, 18405, 19085, 144161, 117918, 8351, 60026, 40557, 1844, + 47924, 56160, 48862, 13071, 86638, 3171, 163462, 48967, 70820, 50635, 8327, 96197, 92206, 86504, 132, 17742, + 86453, 80271, 35704, 19660, 29610, 70884, 187507, 70566, 42241, 55397, 157816, 116938, 119200, 208499, 318827, 57917, + 3198, 33626, 18608, 33628, 15466, 58518, 23680, 48749, 67813, 203805, 73110, 32434, 57863, 126161, 76577, 74704, + 35454, 272624, 56452, 33611, 4779, 612, 20538, 20813, 99518, 12664, 37685, 51378, 4649, 48965, 52644, 7250, + 104641, 90980, 25121, 20782, 144269, 136467, 25473, 109758, 33730, 23835, 64889, 3994, 38073, 175725, 263011, 73296, + 65864, 7458, 91699, 99785, 6838, 11244, 30971, 22298, 109456, 24378, 14229, 234839, 193298, 16188, 31737, 116657, + 154007, 1122, 41881, 49733, 5623, 164859, 73807, 45069, 45741, 8551, 143581, 9315, 30846, 98697, 126198, 189421, + 182578, 54489, 24321, 45654, 25573, 17216, 24178, 85193, 157224, 15399, 12351, 94329, 1543, 110920, 86691, 20245, + 58575, 21729, 399974, 64597, 138703, 15574, 33184, 95550, 146140, 2393, 2271, 17693, 44971, 124299, 48652, 114592, + 49356, 244271, 56021, 82860, 18275, 26970, 11660, 198792, 59064, 6815, 87808, 78781, 20300, 104409, 662, 71033, + 13122, 35626, 44961, 91041, 11848, 14525, 52226, 42701, 24453, 111637, 27557, 12927, 11973, 27925, 2467, 122935, + 9797, 47887, 24976, 6515, 86843, 117000, 127598, 39829, 2919, 138824, 43874, 110700, 25530, 13248, 168387, 43479, + 49210, 1692, 1259, 64697, 1130, 20465, 27466, 19345, 161220, 120389, 31515, 56190, 76788, 22165, 29616, 5113, + 75373, 17538, 30755, 22978, 85604, 112134, 45015, 68154, 34926, 7355, 114461, 64044, 36014, 70882, 20391, 30584, + 17777, 8803, 13476, 33610, 17255, 352133, 26102, 24765, 51533, 55753, 68095, 21188, 11676, 21823, 21179, 271876, + 92226, 107529, 94889, 47154, 51845, 43801, 5311, 105238, 119859, 268539, 2435, 55644, 21525, 37454, 162919, 79553, + 5936, 143734, 13110, 3235, 18507, 21886, 124645, 8664, 28050, 67683, 58054, 52119, 1140, 3546, 35570, 180315, + 31418, 49700, 27671, 84075, 14857, 30098, 18009, 21868, 34207, 42097, 9293, 74669, 47859, 50876, 49991, 60692, + 10750, 72343, 7644, 83181, 36382, 115481, 14074, 68458, 32079, 110696, 30195, 6157, 106909, 22414, 134401, 11947, + 59426, 71942, 7548, 142461, 87757, 25760, 55425, 47637, 38393, 117046, 33833, 33451, 110042, 21631, 15553, 31475, + 15965, 52160, 30794, 68222, 97104, 44038, 134558, 22658, 33757, 7286, 148203, 73358, 35344, 42812, 2789, 141364, + 97993, 325497, 95230, 62242, 53979, 114390, 187, 3414, 33651, 72017, 42725, 163469, 45407, 53268, 119350, 24322, + 41884, 61527, 104655, 61374, 82515, 10912, 127557, 29939, 173089, 44405, 77727, 37217, 7177, 19015, 73371, 191300, + 58371, 10601, 4287, 145829, 35365, 250779, 11615, 1861, 47543, 67388, 153424, 85556, 51927, 90651, 19359, 9654, + 35587, 131677, 91637, 90460, 10670, 58134, 145964, 112159, 23544, 102870, 17599, 26304, 29306, 17111, 10277, 45092, + 84233, 79517, 44634, 85065, 39976, 55740, 13294, 40340, 76076, 274931, 24696, 94204, 62097, 19765, 27791, 72755, + 9007, 11276, 152590, 52634, 8668, 11381, 87423, 17757, 28119, 349, 237, 60867, 78281, 91158, 140967, 248103, + 120790, 33051, 142673, 247599, 19835, 85755, 184690, 18251, 143020, 164693, 4893, 85858, 54968, 19631, 20889, 110604, + 18670, 132107, 13187, 1827, 64959, 187020, 16093, 2357, 20649, 24949, 120227, 112146, 34469, 22861, 29222, 20839, + 42570, 12164, 72533, 58393, 33001, 67590, 100285, 77190, 136570, 1891, 29881, 176839, 87796, 169800, 46634, 42613, + 120044, 544671, 35573, 33409, 1106, 23688, 8382, 40809, 58700, 21997, 89694, 32633, 63951, 5925, 91071, 83353, + 127623, 193205, 8076, 91094, 12805, 5777, 59517, 20986, 83057, 34629, 28371, 28946, 40212, 16089, 140378, 2115, + 31773, 3807, 48370, 178737, 49850, 322390, 73229, 7228, 7361, 34085, 72856, 162851, 54336, 3090, 10705, 24203, + 347524, 3071, 11926, 15437, 101314, 38218, 37603, 25070, 23751, 18738, 10614, 30446, 19569, 34876, 34037, 143092, + 48791, 17269, 13448, 181374, 29174, 22705, 11280, 8389, 49369, 33246, 4494, 15136, 20467, 189070, 24240, 21646, + 7465, 86521, 109202, 104631, 75842, 73950, 26135, 39426, 38281, 58562, 87792, 10755, 623, 98319, 19283, 178647, + 112457, 28075, 23224, 51865, 60210, 1572, 16872, 3984, 28849, 17199, 19586, 53164, 51003, 578756, 51498, 45446, + 94720, 3831, 11364, 15400, 6426, 42807, 26765, 136732, 90047, 18712, 26660, 98061, 85560, 99889, 37338, 10153, + 43761, 188463, 24546, 9883, 3579, 47095, 149286, 1544, 85105, 109163, 22065, 84228, 34607, 9802, 24403, 6597, + 90410, 107034, 41249, 2151, 118528, 32433, 167290, 143308, 7224, 62473, 32534, 855, 42907, 31366, 15790, 130823, + 111163, 23740, 103312, 73946, 18168, 41718, 10722, 74804, 6960, 77903, 6730, 4836, 161135, 460161, 25329, 3966, + 191298, 108138, 97692, 28539, 5247, 14951, 16072, 148552, 100584, 72497, 44704, 114746, 127552, 2033, 34815, 27555, + 171568, 13044, 57905, 30463, 20121, 12578, 29578, 147967, 91173, 69059, 75171, 15963, 12636, 216233, 12189, 78098, + 54615, 17457, 34910, 14101, 20199, 38879, 33868, 12975, 63730, 19371, 122500, 36320, 98105, 44709, 16796, 8252, + 2396, 7493, 206206, 58138, 40387, 10906, 28152, 8026, 14438, 11987, 27633, 84118, 125012, 155087, 126314, 20627, + 4765, 60466, 170206, 93400, 33235, 15747, 658, 8854, 12865, 30917, 688, 103792, 45299, 136720, 88015, 54331, + 37728, 2913, 65993, 80667, 82098, 15958, 29994, 167188, 77872, 103575, 90590, 244435, 114037, 77901, 91272, 19428, + 59253, 30651, 149287, 11214, 19675, 21663, 134751, 84839, 24838, 61313, 45844, 7512, 398016, 64823, 127529, 3133, + 102561, 20453, 115896, 17344, 11446, 222828, 193, 155155, 17069, 58324, 4480, 25422, 57508, 105295, 23785, 108564, + 178277, 20918, 69131, 161769, 65836, 54488, 201783, 143191, 99941, 18413, 13719, 28184, 26114, 27888, 4392, 129687, + 2585, 3092, 113567, 150793, 271882, 1752, 282, 15224, 136866, 70660, 67393, 235271, 50126, 30236, 5205, 12951, + 11027, 106830, 33950, 26602, 155648, 159630, 116983, 47316, 118367, 77639, 2468, 20768, 14585, 66833, 4411, 197715, + 8910, 308244, 4325, 25115, 123015, 105047, 174692, 661, 335383, 65622, 43950, 89084, 40434, 55523, 40872, 29093, + 41016, 46235, 18304, 57207, 53021, 31025, 145373, 39883, 14439, 64867, 33271, 92303, 87098, 165627, 249075, 23882, + 176860, 43613, 45825, 64126, 201543, 92448, 76394, 85896, 121888, 56679, 6043, 5600, 2358, 43170, 38186, 77345, + 9286, 9851, 24013, 78703, 5739, 81394, 113639, 182825, 22666, 9031, 22509, 9570, 54270, 33648, 34339, 13164, + 37884, 37579, 110690, 71903, 169381, 124661, 154669, 17643, 33984, 69534, 35747, 99083, 93859, 18986, 20872, 30989, + 16124, 4894, 119685, 2601, 89364, 45420, 102352, 14665, 72207, 77064, 26614, 22336, 51639, 228, 56231, 815, + 76366, 85000, 4970, 44952, 99029, 11414, 154634, 81988, 65812, 71056, 307722, 32240, 2198, 67495, 76459, 289714, + 12147, 34660, 56034, 21936, 174891, 33766, 38677, 42238, 194289, 61206, 40811, 81549, 6986, 11184, 50356, 28762, + 30252, 169833, 26033, 37387, 88822, 7300, 54514, 41857, 21284, 89562, 16952, 95611, 11445, 78324, 76361, 17313, + 288337, 35719, 74225, 17706, 160821, 46786, 195486, 98124, 33034, 230403, 46596, 54312, 100869, 187581, 73087, 76045, + 43852, 51201, 111095, 53695, 25761, 171167, 1281, 12511, 52882, 77119, 180240, 70944, 1144, 132888, 99788, 35517, + 103809, 160506, 37582, 28159, 1924, 90499, 9703, 89568, 84458, 91412, 201459, 33796, 86079, 85006, 49619, 62157, + 43411, 14396, 37110, 43017, 13542, 75363, 34855, 3223, 139276, 79591, 32317, 66073, 18141, 8975, 111874, 25536, + 34978, 2876, 88258, 8764, 41298, 4941, 10664, 6849, 7276, 16023, 42365, 44065, 26481, 39848, 38615, 3468, + 173800, 385332, 27782, 6783, 33210, 23625, 31896, 1982, 17951, 11857, 55263, 92496, 142652, 13696, 62877, 77106, + 33616, 34409, 3165, 42139, 33677, 25816, 8589, 110980, 2210, 30731, 11059, 25363, 19941, 193105, 164524, 15578, + 98568, 36064, 29325, 13286, 2486, 12135, 218797, 3219, 192414, 393107, 34699, 89750, 80136, 7124, 7367, 13443, + 12058, 69118, 234202, 17915, 235883, 20792, 100851, 31528, 50963, 3680, 2664, 124375, 249638, 8483, 257047, 41605, + 29572, 29737, 139767, 51651, 27221, 6765, 55803, 23145, 47034, 40480, 52532, 73864, 6124, 42229, 93325, 34530, + 72107, 238280, 199709, 3744, 63346, 16597, 66408, 22715, 97620, 5271, 1410, 22445, 158513, 169512, 31624, 107883, + 299699, 50048, 63128, 87490, 40388, 185087, 19754, 75917, 23235, 138863, 325617, 37883, 37176, 65115, 41352, 25967, + 224244, 118096, 25013, 205505, 198386, 69311, 49810, 112803, 121323, 27224, 31934, 41103, 67992, 90172, 18343, 182947, + 23827, 233481, 44894, 9617, 63170, 38593, 111112, 18189, 17838, 11885, 38329, 7604, 106622, 67890, 139944, 6251, + 158590, 31160, 39376, 75979, 26807, 59454, 75828, 12609, 5345, 62668, 13410, 6377, 23489, 15227, 50336, 23847, + 91891, 46989, 219110, 5016, 55474, 182, 169668, 41243, 74834, 37258, 81806, 25477, 37981, 32374, 29946, 8558, + 13058, 27278, 55639, 110342, 5977, 7496, 7827, 224669, 72552, 8581, 18359, 28445, 34706, 45938, 138729, 19479, + 26828, 4897, 199990, 7309, 145172, 26292, 10057, 2903, 19904, 3127, 7625, 8343, 21367, 5265, 8513, 8299, + 34043, 7029, 6384, 111718, 960, 4780, 109654, 50272, 77092, 23412, 109010, 40059, 91381, 138810, 25275, 30422, + 4733, 94279, 5863, 2603, 47446, 7973, 33416, 25502, 7680, 106096, 17414, 15137, 41697, 38583, 90939, 13115, + 5170, 1287, 11657, 96186, 16960, 66479, 61042, 54454, 14741, 104736, 18646, 28260, 46101, 248526, 78951, 52606, + 13656, 58251, 8482, 69402, 473, 134516, 4405, 18865, 51842, 100181, 26348, 80528, 37433, 55053, 30045, 136822, + 11103, 22444, 11841, 2990, 11551, 36343, 57239, 17946, 121951, 165051, 7702, 15912, 13191, 61072, 26908, 5979, + 97536, 32603, 54072, 112162, 165932, 27730, 13979, 91093, 50397, 48878, 44400, 29260, 51628, 17193, 15977, 23879, + 129028, 208297, 58084, 29487, 9069, 58477, 73687, 7734, 44885, 223955, 46203, 40661, 6590, 253832, 62105, 27627, + 59195, 37610, 112, 160041, 47045, 121276, 9957, 89691, 32940, 13845, 859, 21447, 225472, 109616, 5172, 115309, + 90345, 174021, 7312, 26518, 21833, 129351, 285466, 54661, 13303, 119359, 7473, 179961, 29407, 61141, 37403, 357673, + 96615, 35776, 100714, 58390, 141951, 44340, 133721, 168376, 5198, 37474, 20461, 28860, 6028, 3028, 13118, 40061, + 18395, 65200, 55843, 156099, 7181, 326625, 72811, 24544, 3861, 106507, 15886, 80513, 14966, 54808, 143914, 131660, + 156358, 72569, 331, 115499, 167182, 181285, 3231, 35925, 36529, 34503, 18991, 46621, 55253, 10258, 55965, 813, + 25942, 89419, 48957, 177707, 173153, 46642, 4811, 91950, 30959, 57953, 55844, 6837, 27261, 33866, 171253, 83769, + 50691, 10414, 5492, 45302, 150176, 127189, 25506, 98266, 162201, 46921, 47463, 25896, 38467, 46851, 18084, 3144, + 48462, 72055, 57402, 19107, 80602, 38235, 64308, 11648, 42163, 101559, 80727, 54159, 118482, 153426, 60818, 128542, + 168, 55184, 5394, 2574, 108756, 27110, 245250, 38029, 26011, 2085, 2189, 19738, 17166, 17187, 129874, 85131, + 54149, 86936, 135307, 122042, 456538, 40725, 3718, 195077, 22512, 53925, 52733, 40639, 91374, 71487, 56427, 22962, + 13816, 20316, 44904, 29393, 90358, 36347, 18997, 57794, 131615, 11502, 90717, 6758, 18132, 32540, 226257, 10712, + 226707, 53602, 99511, 19231, 1824, 19111, 49236, 5491, 28139, 42348, 17387, 13741, 26860, 32136, 143030, 11826, + 42253, 125128, 48221, 24174, 93877, 39491, 28952, 24227, 77351, 64398, 63400, 162461, 65575, 4012, 37187, 20132, + 8980, 42178, 52118, 64518, 80574, 106352, 75873, 68981, 22020, 35576, 63767, 70957, 27948, 62633, 6166, 497, + 40422, 23112, 21321, 14642, 91324, 90681, 29471, 53428, 76376, 17160, 5165, 158982, 13528, 21170, 4421, 11861, + 39281, 97681, 28741, 5107, 91685, 14451, 28300, 33929, 82215, 202223, 39186, 1108, 122541, 3164, 84493, 54892, + 144066, 56213, 6189, 105740, 1983, 53506, 28897, 52102, 193851, 154542, 51373, 38315, 17283, 44071, 149080, 48489, + 26320, 80807, 30857, 143431, 2739, 197396, 39482, 10242, 194978, 39273, 69728, 108587, 4790, 80763, 38090, 13241, + 26845, 225930, 45466, 7671, 42627, 235691, 55444, 50456, 61300, 2137, 111458, 41994, 65815, 20573, 171738, 111385, + 174612, 46292, 37295, 150555, 55133, 45791, 85658, 132663, 4200, 13863, 247261, 33106, 191130, 68764, 69933, 342026, + 79771, 57623, 102440, 82923, 158321, 74104, 66775, 232997, 52280, 5348, 14740, 63482, 166796, 21974, 61836, 39710, + 221620, 16509, 20155, 122691, 62461, 15494, 286059, 74491, 11278, 173634, 24814, 36352, 4067, 124651, 6219, 20384, + 88152, 106522, 11199, 27155, 83409, 59291, 62619, 7943, 31717, 82823, 35872, 25490, 121367, 16822, 5527, 43809, + 13522, 275353, 25968, 13784, 47325, 66250, 55180, 23370, 37945, 34951, 32887, 154415, 10406, 26787, 7574, 51785, + 174348, 5257, 63098, 12141, 249321, 18164, 175374, 159625, 154101, 6386, 36436, 10514, 64912, 129913, 42505, 64489, + 29938, 34866, 92162, 115463, 51775, 138015, 32129, 31108, 17220, 19470, 60959, 82863, 15776, 2068, 11894, 44229, + 166138, 59776, 2329, 138779, 78890, 11618, 39616, 3684, 84425, 73187, 5203, 51002, 54121, 48875, 276201, 108655, + 42861, 116287, 106861, 140810, 16368, 27367, 102464, 4845, 24572, 65525, 25498, 65011, 291647, 3490, 34570, 87715, + 10197, 173917, 12769, 8636, 32073, 8577, 38657, 12073, 22651, 98887, 35637, 26878, 11677, 114271, 87008, 92497, + 97509, 14575, 3470, 58305, 26952, 16841, 8381, 10555, 35787, 2648, 41602, 77764, 18424, 35932, 45851, 49096, + 41910, 7650, 71685, 129774, 71614, 52658, 36248, 19880, 94977, 39129, 145464, 57624, 72318, 30245, 113156, 32799, + 41594, 8407, 15488, 66070, 70024, 38697, 26127, 49773, 275419, 9728, 21901, 111141, 37702, 136166, 21682, 76474, + 60199, 7085, 79133, 215800, 7335, 50628, 141287, 17217, 39107, 44612, 205482, 35296, 61315, 75127, 44962, 2175, + 18271, 83503, 115273, 114695, 18394, 122374, 164929, 11745, 33768, 52043, 39554, 3954, 87884, 6547, 14314, 26459, + 104277, 94471, 129578, 91248, 123724, 20555, 12338, 148214, 7277, 42970, 32692, 38110, 56288, 19752, 90889, 130277, + 71981, 95103, 10470, 106893, 189803, 47422, 67706, 38984, 49320, 67270, 32034, 67179, 3352, 105490, 2902, 57799, + 6798, 57302, 88662, 2520, 14240, 632, 64114, 111171, 8954, 67696, 178121, 64478, 69220, 98726, 78181, 52577, + 94433, 48703, 92812, 106819, 57372, 970, 11507, 56315, 28620, 13927, 5879, 50384, 68863, 811, 54518, 38111, + 193727, 4518, 82041, 45997, 85575, 141392, 39464, 38164, 42309, 34939, 27631, 115200, 41667, 5852, 85451, 45254, + 67689, 36959, 69349, 25516, 42081, 284, 1617, 24389, 22543, 92428, 55862, 39478, 44824, 158788, 112673, 24864, + 12719, 95525, 421417, 153017, 28540, 12854, 40525, 3447, 114236, 119912, 41795, 7482, 101553, 14084, 90262, 98146, + 27638, 309738, 63986, 26332, 27296, 73457, 26543, 61153, 4300, 19919, 75492, 157204, 5353, 16531, 61956, 47675, + 4663, 113612, 136374, 222705, 19379, 3505, 93057, 31, 94098, 199552, 229445, 75586, 3758, 9803, 54043, 51022, + 95888, 418251, 47815, 8325, 95144, 54354, 55865, 238684, 80344, 14773, 42431, 26078, 87320, 4173, 49174, 59477, + 28447, 53727, 59450, 37425, 259518, 260604, 13221, 59388, 12718, 19200, 54560, 211, 71391, 111794, 43082, 14317, + 152731, 24043, 16563, 55318, 37063, 33985, 12107, 8451, 24132, 3287, 51633, 24662, 31911, 94583, 27566, 47306, + 104896, 123698, 17450, 4892, 15672, 1239, 135524, 82674, 103782, 128381, 195863, 42040, 1521, 88669, 5368, 61959, + 4945, 14280, 54416, 134709, 72541, 71947, 141565, 31806, 23717, 13486, 49292, 28755, 122632, 37972, 227115, 71973, + 15619, 45930, 73185, 19728, 87175, 41028, 113786, 71313, 206120, 15801, 80915, 37045, 29428, 213276, 42087, 78562, + 189780, 69074, 397153, 114057, 61416, 106834, 67699, 184163, 28350, 15478, 41280, 87632, 44457, 50713, 90885, 28916, + 972, 63102, 58749, 38921, 1175, 182790, 133419, 33965, 47233, 11089, 17346, 24241, 198738, 99658, 3632, 15062, + 95789, 46049, 55098, 80139, 41907, 66419, 62949, 77436, 21953, 25574, 115070, 31261, 97034, 86959, 15541, 120250, + 59341, 34977, 37912, 95547, 22864, 57455, 27137, 114631, 53713, 28129, 16277, 219371, 16873, 48501, 25135, 20596, + 32971, 2044, 70095, 43252, 20693, 70672, 5134, 139706, 20954, 18793, 5240, 51062, 31336, 1055, 9964, 20812, + 21477, 94661, 40609, 21902, 16169, 19574, 74742, 44447, 38370, 72501, 159022, 27749, 16412, 12007, 11867, 64559, + 9019, 60758, 6521, 41890, 3841, 1011, 208127, 23460, 24599, 115489, 30488, 57116, 21938, 126419, 279459, 210650, + 17085, 29349, 117824, 4642, 6484, 24363, 70018, 30366, 81198, 51053, 57403, 18554, 76413, 87591, 130889, 12473, + 5849, 12616, 44081, 17726, 72514, 20574, 39804, 77427, 12320, 153366, 63071, 43010, 65247, 12837, 49822, 119883, + 276175, 48298, 17891, 55934, 37234, 15426, 536, 214834, 59796, 107143, 73492, 82284, 52642, 23860, 59584, 109240, + 16312, 295305, 2881, 141523, 57349, 24996, 10169, 27023, 198507, 100921, 101928, 19612, 94148, 193262, 51722, 22594, + 46134, 59320, 233123, 23163, 18958, 48350, 10418, 11573, 125552, 158579, 54776, 71219, 1747, 9488, 45024, 123446, + 18725, 52331, 24040, 29879, 151873, 17176, 22311, 178292, 14901, 31482, 26423, 45056, 5490, 10022, 15757, 97024, + 68287, 99243, 207125, 128979, 29470, 1325, 74812, 32791, 3689, 45845, 118509, 34820, 64794, 70223, 8344, 91384, + 40814, 104345, 56330, 22095, 26018, 85129, 77063, 49913, 25692, 80443, 48676, 207462, 54450, 117644, 131820, 12098, + 2703, 16863, 18276, 60530, 88278, 81796, 11213, 17129, 124886, 4875, 8932, 23106, 173087, 7396, 71377, 23220, + 174000, 24872, 76210, 196270, 24159, 83016, 95481, 92620, 179477, 142594, 74941, 14268, 24276, 115069, 15141, 25430, + 46004, 119419, 64735, 171433, 201876, 166502, 13507, 2133, 209202, 8831, 250649, 58555, 445, 79606, 10547, 18957, + 52876, 93525, 47741, 109879, 31948, 69285, 97122, 68070, 30206, 36316, 27294, 147592, 157610, 357846, 4949, 3838, + 39180, 165668, 28395, 105564, 18439, 113339, 26143, 6254, 44124, 41027, 149595, 57880, 50469, 74956, 105797, 64751, + 5774, 62996, 55064, 12300, 96278, 74378, 41632, 28378, 222758, 215455, 14905, 29733, 200216, 83974, 14267, 197651, + 50290, 108173, 83523, 72906, 45486, 17894, 248112, 6668, 20435, 12354, 69859, 105672, 46986, 26269, 26119, 21735, + 46276, 81332, 161990, 24229, 140133, 80736, 85948, 28342, 142326, 114859, 5246, 12288, 15569, 321372, 83346, 67317, + 13363, 11347, 62559, 87384, 47522, 66304, 51125, 158071, 92583, 215430, 30981, 130176, 2182, 17025, 35860, 41627, + 7135, 192109, 213, 29142, 16853, 130975, 2389, 127400, 22998, 131988, 9785, 68168, 30272, 21382, 58736, 6997, + 4952, 39834, 32713, 104019, 63263, 581, 147846, 14035, 35623, 7875, 177579, 12052, 39096, 112656, 33118, 37277, + 53789, 60622, 157938, 185910, 44864, 30132, 308910, 81836, 20053, 20029, 111, 252367, 110392, 9585, 162293, 4213, + 124213, 140484, 19392, 33595, 4630, 45380, 23884, 137937, 16087, 21464, 32146, 130095, 28221, 147475, 40847, 37757, + 127787, 95424, 105555, 146520, 25839, 9169, 5255, 99477, 77481, 245575, 97240, 7618, 44693, 52011, 5049, 29327, + 13464, 195851, 8615, 52596, 113146, 3124, 234482, 38343, 6983, 249017, 62799, 87690, 27069, 6892, 7757, 568, + 55717, 67952, 55524, 29469, 50102, 116514, 63808, 119487, 4760, 11374, 79868, 17622, 7107, 13396, 118343, 202733, + 26186, 94968, 133457, 113546, 66507, 11011, 141426, 116015, 59145, 7451, 3054, 4656, 36032, 68955, 55309, 29753, + 104182, 23389, 82478, 44486, 71328, 86912, 16831, 60480, 29425, 22716, 53199, 42308, 64317, 88346, 22804, 101981, + 50781, 6916, 20926, 87069, 47465, 22345, 6416, 67964, 94298, 12161, 198305, 25527, 69706, 1141, 24861, 18820, + 74899, 101908, 136290, 36246, 22754, 43947, 149419, 77020, 120756, 58182, 76675, 53183, 25108, 141513, 334998, 81890, + 93077, 30790, 76148, 97326, 56834, 21494, 3126, 13675, 73286, 10835, 21018, 39793, 39928, 69833, 40373, 1638, + 16218, 27262, 46999, 35926, 41699, 14586, 109707, 10621, 176763, 65754, 4781, 40629, 7555, 38881, 34586, 20380, + 70819, 99768, 116580, 11114, 50083, 71750, 38765, 26763, 26895, 31093, 26106, 99244, 23315, 195234, 103007, 80697, + 26014, 69431, 24523, 14850, 16773, 129449, 83866, 113767, 123079, 183143, 1343, 35751, 41712, 7818, 21857, 75865, + 5719, 13588, 11322, 41995, 31516, 21912, 16746, 20696, 90427, 100022, 97349, 50603, 158540, 42138, 33822, 20310, + 85051, 198477, 100819, 31299, 183128, 37925, 83454, 48059, 40864, 109756, 117963, 246050, 27505, 125055, 6202, 12888, + 55392, 82049, 6852, 20486, 9058, 55998, 15942, 21876, 45224, 30137, 11302, 33518, 96857, 5033, 17578, 243172, + 30901, 1136, 98132, 67204, 136622, 53361, 185908, 164211, 96557, 1199, 46191, 6810, 56304, 16854, 41481, 31638, + 120061, 167078, 70451, 36778, 11501, 72634, 53232, 33096, 151448, 12676, 107140, 3255, 5773, 230373, 199725, 58707, + 89743, 159601, 29117, 51821, 7769, 175079, 179962, 14736, 86069, 12406, 35599, 12585, 2935, 122863, 21218, 92679, + 18471, 74106, 23743, 2268, 41628, 25025, 251009, 101461, 10114, 69681, 874, 844, 33660, 84276, 20996, 3116, + 110170, 3629, 33273, 374091, 49479, 7043, 8134, 1695, 26745, 1439, 1061, 171360, 92846, 117704, 95171, 30559, + 33221, 6627, 172996, 24530, 26731, 509, 15456, 63235, 18795, 30005, 53873, 51891, 87076, 62196, 32574, 96562, + 8550, 98665, 117502, 67674, 2100, 12527, 40235, 66878, 29972, 78874, 26467, 41590, 120289, 181416, 78604, 54157, + 3077, 84697, 134742, 91234, 72490, 15005, 76558, 55084, 33784, 162703, 6048, 46791, 2630, 127835, 19594, 122511, + 208722, 193416, 9502, 8107, 50861, 143793, 44636, 51976, 63483, 12325, 10412, 23264, 79029, 29050, 159857, 149078, + 6419, 154772, 107400, 107603, 39467, 13028, 84919, 63134, 14302, 158425, 87104, 88768, 45286, 22612, 34903, 13577, + 64207, 6221, 59147, 11798, 9686, 121962, 135449, 86848, 67513, 17167, 43511, 68844, 44170, 71147, 44786, 64366, + 1050, 10887, 190612, 21896, 77246, 77296, 70814, 135434, 59266, 18452, 133, 55042, 17055, 1640, 13034, 42496, + 53801, 5748, 52414, 66381, 7150, 144739, 6440, 74993, 11111, 2539, 50363, 23303, 42432, 27028, 66935, 13005, + 4278, 7311, 46716, 3338, 94579, 8115, 26937, 50962, 362117, 30782, 3762, 141892, 36175, 73088, 50180, 37005, + 42902, 253122, 113704, 91922, 41933, 43732, 105477, 3520, 39002, 3843, 42324, 258344, 98489, 29853, 56586, 11607, + 22913, 43149, 12984, 35738, 74161, 6039, 61803, 269, 84773, 58569, 22403, 44259, 57036, 31666, 126796, 12483, + 17556, 38761, 298166, 122446, 162288, 3950, 44945, 1370, 74485, 97973, 26528, 36641, 178760, 75233, 37361, 147382, + 93867, 98504, 161890, 33435, 73635, 18503, 26688, 55952, 128860, 76113, 36649, 15218, 50362, 50874, 136633, 104263, + 261, 187132, 5194, 41473, 67455, 26709, 46683, 61196, 80001, 415, 103032, 77008, 46080, 63776, 21671, 45605, + 35662, 12969, 32724, 41546, 4368, 25676, 78170, 10132, 25247, 21941, 10589, 88199, 19230, 36489, 23652, 71018, + 74393, 15514, 33003, 61628, 22588, 82874, 278, 656, 1822, 7365, 51787, 44718, 27682, 7842, 148545, 22113, + 235324, 53467, 25889, 37986, 13798, 8780, 14653, 79341, 85998, 58114, 38940, 70133, 13194, 10663, 186560, 72895, + 235067, 15731, 34281, 180158, 23514, 60239, 132955, 17621, 71669, 107863, 209492, 4929, 147632, 35364, 73172, 45463, + 23191, 35596, 21865, 59198, 134748, 84141, 128176, 15559, 214683, 7375, 153174, 69569, 105101, 54279, 191537, 11893, + 1518, 28125, 88836, 27303, 25489, 46180, 96736, 5887, 247114, 5137, 287773, 60728, 7380, 108022, 182042, 30064, + 54842, 72963, 28745, 42623, 26922, 16894, 8922, 6003, 3971, 130326, 30795, 15767, 26361, 58938, 27324, 20292, + 20844, 29628, 16534, 159213, 68642, 15346, 219023, 63240, 170517, 8331, 15673, 3213, 77339, 151668, 65928, 33858, + 123255, 106689, 30575, 26185, 8963, 12688, 15792, 24737, 77818, 92544, 7997, 20221, 150998, 55663, 1268, 41573, + 48466, 14085, 128978, 65797, 36806, 28519, 69465, 20974, 2732, 41172, 202748, 116152, 23261, 39001, 2280, 32931, + 11741, 66879, 195696, 31356, 236162, 62810, 25653, 37741, 18243, 31739, 43296, 15723, 126216, 75117, 27208, 74878, + 28690, 17377, 22841, 46221, 50546, 479, 9735, 5075, 16385, 17152, 9080, 33925, 92760, 24705, 35011, 52286, + 197383, 118668, 24200, 32927, 246558, 83210, 49673, 39479, 201295, 11697, 23650, 58791, 88255, 2117, 58010, 136860, + 67588, 5287, 34543, 6591, 71687, 95613, 48832, 64315, 176076, 18307, 105134, 12037, 172653, 140943, 36060, 3370, + 169058, 87901, 2424, 35703, 33906, 68007, 83459, 86267, 63747, 78729, 15829, 39429, 24835, 60607, 1063, 942, + 157621, 15510, 142744, 36875, 43338, 26941, 6283, 201368, 30050, 1294, 14144, 28874, 46152, 163373, 100423, 33959, + 132741, 10200, 30369, 5793, 2770, 40793, 66426, 145294, 51371, 9412, 47667, 53918, 94835, 47111, 93658, 291281, + 6614, 6818, 28373, 98899, 15112, 55868, 85946, 13126, 11749, 15201, 6184, 52292, 56936, 9994, 67564, 15398, + 1250, 16480, 28355, 50093, 19027, 134101, 912, 36390, 399017, 67061, 175796, 31206, 58036, 37028, 36592, 15922, + 100215, 155543, 7324, 4771, 23388, 157277, 186074, 20469, 55815, 15438, 73729, 36924, 308768, 3933, 6366, 20641, + 124152, 60772, 12026, 70045, 94803, 6290, 19858, 1915, 9521, 22497, 33912, 49717, 64186, 47263, 9814, 19866, + 8971, 350258, 314, 10683, 28, 6135, 16425, 48283, 30427, 224788, 96210, 41227, 62163, 9112, 237935, 8329, + 7616, 14660, 20925, 152205, 103838, 6480, 53909, 29003, 35079, 21715, 38510, 2096, 29203, 37569, 47676, 30859, + 131235, 66331, 56052, 67144, 7743, 65717, 38496, 26265, 17389, 72433, 5984, 42527, 10882, 140995, 248537, 4000, + 37420, 43361, 72768, 79706, 61460, 44601, 88348, 120824, 228512, 92578, 101207, 2506, 85363, 72057, 112263, 74889, + 41581, 61184, 59336, 124955, 131077, 388, 24445, 445574, 62822, 10339, 54594, 139384, 119647, 26960, 115230, 377822, + 10130, 53380, 25507, 4582, 54445, 4045, 113722, 79437, 26925, 51571, 10619, 37744, 19968, 21756, 62099, 38841, + 29016, 19474, 28660, 169417, 24446, 77906, 53823, 54729, 74028, 4315, 3444, 12379, 24176, 2062, 118391, 71991, + 61448, 24221, 58190, 114666, 67185, 84137, 1932, 38777, 9254, 63804, 23453, 23502, 8563, 53758, 17591, 83661, + 119129, 33378, 156031, 31341, 9771, 4905, 245, 10643, 99184, 71196, 20709, 250, 37716, 19394, 203310, 82339, + 39514, 27829, 5347, 68674, 10532, 102550, 189900, 41082, 221512, 57643, 21885, 60429, 258753, 28243, 26729, 38284, + 218630, 266776, 74708, 10059, 55980, 59074, 26095, 4002, 23394, 34908, 56295, 38826, 32141, 56657, 44390, 129016, + 61924, 77979, 141893, 16627, 66749, 173128, 78650, 84113, 32411, 36734, 83212, 22287, 3741, 109048, 15156, 33529, + 36475, 217436, 48727, 82121, 26678, 67771, 256285, 2700, 77010, 79442, 5038, 3136, 44946, 56358, 46209, 4267, + 91203, 9096, 96644, 19035, 128749, 10636, 6976, 205036, 116953, 56466, 63959, 18341, 20476, 42517, 7840, 100552, + 49625, 4375, 77579, 19118, 53116, 3012, 35805, 64719, 13735, 124583, 30702, 85109, 102335, 116046, 63278, 101038, + 29376, 131644, 18364, 4281, 51946, 89017, 31230, 164451, 83407, 14320, 34509, 23271, 67892, 72729, 37652, 77746, + 59212, 14913, 6854, 43898, 34685, 72734, 50838, 3371, 21083, 24922, 49503, 29227, 1546, 61493, 17037, 10316, + 112982, 4328, 38907, 93116, 32972, 99365, 223827, 37012, 74397, 3821, 103422, 35362, 1078, 29713, 94154, 55450, + 190545, 68894, 29500, 75558, 16082, 49117, 103414, 107471, 86140, 770, 35589, 44869, 58591, 17981, 10817, 9420, + 89611, 22016, 15994, 34959, 101531, 126914, 193257, 72721, 10061, 73572, 85338, 101867, 105104, 609, 98863, 73482, + 76319, 100600, 207540, 8308, 20035, 8093, 56554, 15585, 17551, 38570, 177750, 85937, 52611, 10767, 28909, 26249, + 169061, 139097, 59137, 254690, 190842, 27037, 47208, 1901, 100780, 278291, 22166, 32105, 23907, 107009, 147748, 23093, + 90413, 43974, 38278, 110542, 115619, 45653, 24331, 51759, 9675, 125197, 28009, 227009, 34710, 181128, 25798, 132667, + 193435, 41954, 44477, 110078, 49443, 28528, 66593, 13781, 129734, 5325, 109119, 17206, 11183, 17837, 41403, 199989, + 258877, 23595, 49436, 2482, 16318, 60636, 117129, 70004, 136182, 100062, 20218, 28137, 126808, 127896, 48962, 38967, + 44635, 13158, 93741, 10921, 27304, 68089, 142263, 18325, 192375, 147811, 36115, 47851, 2599, 12879, 123482, 145544, + 125648, 78600, 106709, 37509, 47051, 31245, 9380, 153218, 12091, 99206, 351089, 1706, 23814, 20083, 2942, 45798, + 721, 22708, 105601, 201509, 58800, 153251, 16149, 130340, 40137, 47023, 45551, 84104, 66726, 85042, 67373, 116656, + 97930, 21507, 18614, 49333, 60877, 118514, 56360, 10125, 74487, 128507, 90887, 17233, 7942, 46505, 12104, 513, + 54326, 57737, 60599, 113700, 9841, 11073, 24431, 42281, 41428, 3734, 51341, 225984, 13762, 7257, 11599, 104571, + 8211, 44012, 104316, 48008, 85383, 17867, 24242, 577, 6950, 151859, 2565, 40033, 99177, 174326, 186646, 2995, + 79806, 4196, 14521, 60729, 201786, 35248, 27115, 28097, 296464, 53923, 41708, 44679, 124087, 83378, 146584, 6497, + 13144, 70640, 20047, 27733, 29741, 53377, 153924, 19142, 41721, 171276, 66163, 88810, 47634, 5092, 38780, 86108, + 55088, 32716, 141186, 15641, 254286, 116055, 26764, 59396, 106408, 75258, 2560, 73860, 17041, 253752, 52211, 39488, + 99064, 95466, 64462, 11423, 12942, 41175, 93052, 29798, 64086, 46186, 33800, 33567, 45233, 66006, 7617, 49299, + 14005, 40955, 150448, 239881, 2612, 82651, 30016, 5178, 55827, 9423, 94272, 251540, 255, 30751, 103573, 11587, + 7984, 28977, 4978, 95968, 13980, 47836, 58308, 50268, 38574, 77347, 20931, 57083, 12776, 22503, 10, 4635, + 46654, 154112, 11869, 151047, 73499, 9650, 31746, 60983, 249951, 39416, 25878, 43811, 2101, 9653, 7416, 8737, + 26676, 92346, 181430, 83072, 25996, 158181, 85015, 37325, 132326, 48445, 2731, 75518, 116415, 209483, 32511, 38210, + 119062, 17333, 2785, 908, 50449, 214116, 161693, 5897, 31033, 187419, 60336, 5447, 23038, 9049, 23426, 57262, + 11589, 1592, 18499, 5286, 179252, 44973, 418, 77691, 20007, 18386, 42112, 52950, 14860, 1598, 187402, 62235, + 129270, 92667, 2326, 100310, 21143, 53140, 34792, 111283, 17796, 65259, 194012, 97011, 144715, 35840, 20371, 15935, + 60106, 189595, 22778, 41157, 70758, 50788, 46106, 29863, 69842, 86840, 30479, 14570, 34674, 67390, 15509, 71299, + 282133, 2275, 35835, 109932, 44014, 100391, 67192, 15948, 16774, 13637, 53829, 16317, 57268, 94004, 20544, 25822, + 38528, 40203, 28555, 97510, 24053, 21113, 6021, 47281, 46373, 2496, 116133, 176010, 201667, 28820, 53091, 166496, + 28327, 26507, 34663, 247773, 471023, 17682, 2427, 24715, 51889, 11389, 166917, 3466, 102667, 63097, 164910, 47310, + 21193, 150917, 3081, 121294, 114909, 56277, 57524, 64525, 84132, 17553, 63486, 76104, 69317, 55368, 502, 4853, + 96723, 70125, 25212, 69051, 67969, 36687, 75249, 1403, 16134, 580, 2956, 41676, 68145, 22459, 93435, 124068, + 15058, 46025, 62695, 17614, 28765, 189125, 1647, 15184, 32035, 23120, 137691, 51605, 2524, 74673, 6620, 207114, + 101089, 36259, 21019, 104217, 98664, 31074, 19082, 94463, 25045, 6564, 91038, 90673, 76571, 79552, 64302, 92382, + 14957, 61083, 144594, 201758, 86040, 109363, 266748, 12661, 118506, 125644, 159814, 57896, 262428, 108888, 87913, 33717, + 154764, 294744, 43549, 58731, 81573, 67852, 24804, 51538, 39681, 122957, 62858, 15248, 283900, 55535, 49196, 35328, + 73287, 114610, 61587, 16985, 127825, 28981, 37479, 9256, 544, 41344, 20620, 91193, 80448, 170849, 59318, 7633, + 52347, 121720, 45439, 11408, 38512, 20264, 4581, 36309, 175971, 26347, 10413, 16235, 15180, 35078, 30388, 152653, + 45467, 29969, 183795, 49439, 33086, 1929, 164867, 88587, 46552, 130665, 18076, 34437, 48894, 15770, 53144, 83762, + 81107, 66843, 19430, 136312, 43213, 23986, 22371, 51721, 36672, 73932, 85044, 11462, 54025, 63006, 70924, 28412, + 76703, 388659, 28510, 37525, 8053, 29403, 351574, 243678, 7608, 105640, 74981, 222745, 13299, 69352, 22764, 32848, + 56619, 140685, 29353, 106, 20752, 4501, 61795, 68153, 238099, 39552, 89245, 17454, 54164, 23662, 42008, 59724, + 105133, 53821, 26404, 115768, 1444, 16209, 287358, 17881, 32942, 78671, 61192, 56974, 953, 17778, 20882, 55194, + 37564, 73360, 211669, 11594, 8000, 109829, 67377, 21481, 66316, 204718, 32898, 37701, 119463, 6868, 32788, 5503, + 106817, 232653, 56662, 123157, 404, 44879, 169840, 19912, 13667, 10522, 13222, 180347, 149108, 31852, 19954, 1455, + 128597, 19388, 66139, 13463, 31267, 28564, 85407, 118622, 10269, 12637, 135119, 151455, 49836, 122605, 44182, 26588, + 106150, 14664, 171949, 1452, 1484, 40891, 43483, 32813, 52330, 160046, 414611, 4668, 76965, 52847, 285294, 29777, + 160486, 19187, 64830, 245534, 171648, 8708, 16151, 96632, 38456, 197248, 3824, 13111, 31263, 5534, 22810, 94095, + 22424, 5060, 6994, 76043, 37738, 54013, 153414, 28274, 66245, 103049, 7220, 15850, 67467, 48469, 60783, 177423, + 143369, 15480, 20191, 1782, 60471, 187319, 90210, 9498, 75610, 1006, 245177, 1892, 20895, 6738, 21020, 52235, + 115528, 104750, 54596, 6369, 86070, 14562, 167100, 84334, 60854, 23828, 51465, 49525, 40796, 89711, 108733, 53141, + 49347, 11699, 22079, 52616, 18989, 60426, 6070, 1322, 15030, 77286, 28845, 5836, 11371, 49753, 49923, 40348, + 37578, 73337, 2788, 68945, 15779, 203365, 40093, 11808, 79867, 81426, 46442, 9689, 10187, 52258, 15730, 33729, + 86462, 49418, 30284, 16818, 46402, 43558, 19285, 95141, 155626, 31136, 296724, 58803, 93200, 46488, 332562, 48870, + 40229, 30569, 5173, 69228, 7090, 28830, 105171, 66711, 57547, 57695, 42695, 76635, 108053, 24676, 92847, 18249, + 99598, 51389, 17912, 84688, 11088, 33411, 178627, 569, 47505, 18773, 121108, 7263, 41218, 129818, 35668, 32165, + 206017, 146881, 10066, 21894, 2173, 12265, 27741, 23761, 20988, 8052, 179620, 44251, 30219, 107113, 49515, 5809, + 22919, 43643, 10121, 20448, 80563, 119663, 169374, 59245, 57566, 90682, 12457, 225388, 42369, 203562, 11662, 128551, + 93141, 84259, 24761, 94597, 41675, 122505, 212284, 48603, 2407, 9599, 7883, 24703, 182519, 107518, 90911, 22385, + 120495, 22791, 32676, 56812, 27154, 24521, 13655, 41800, 16702, 262168, 63509, 14150, 29456, 135382, 45733, 66046, + 14349, 2518, 233250, 50438, 7958, 21556, 8312, 32247, 16688, 7974, 4721, 4342, 117177, 13427, 43940, 123614, + 140375, 20924, 42414, 505, 42467, 36757, 55097, 32118, 261919, 34892, 58385, 134010, 74916, 2566, 138977, 120089, + 153569, 42388, 97409, 75482, 10836, 123, 5341, 33838, 34742, 48578, 76395, 92995, 49526, 37105, 106505, 72144, + 7621, 24215, 152644, 48127, 105997, 73105, 87109, 52037, 12212, 625, 111988, 112734, 2270, 76628, 35699, 44168, + 392377, 67240, 91475, 67254, 7755, 119314, 9723, 6967, 17959, 185692, 25707, 36302, 25086, 109996, 7225, 112068, + 232152, 122120, 101654, 13640, 138791, 16408, 39845, 8399, 33847, 12887, 152461, 34536, 13860, 12517, 180090, 169472, + 35316, 3208, 52910, 286726, 5811, 60049, 6687, 6745, 1344, 108692, 23669, 20503, 71259, 58644, 186034, 23770, + 50452, 17374, 5900, 712, 207539, 154425, 93220, 54448, 92635, 125802, 14285, 77361, 50359, 69288, 133264, 162621, + 5821, 93205, 28457, 129771, 33674, 8402, 51971, 38768, 30255, 195827, 18512, 68308, 2086, 8475, 44179, 212, + 2587, 255482, 11233, 42032, 96264, 234156, 71743, 9619, 17543, 9966, 59340, 53, 42, 51576, 68365, 150251, + 6029, 116729, 63303, 1303, 9580, 56310, 126033, 11299, 43007, 25304, 11348, 2202, 139248, 211176, 10147, 4290, + 82831, 107660, 57933, 177074, 12917, 54254, 36738, 72091, 29607, 42295, 47993, 166376, 25786, 73979, 352922, 17657, + 51467, 73749, 5917, 82140, 42137, 39138, 697, 49880, 85161, 40070, 149172, 172144, 100698, 83192, 48718, 29859, + 31561, 21429, 53401, 29518, 88989, 43651, 46656, 32160, 121990, 32912, 74292, 57977, 278500, 63671, 75205, 23517, + 3602, 60467, 33461, 137178, 109344, 49843, 1353, 103161, 37982, 43271, 19531, 62950, 15279, 34216, 34547, 113009, + 116442, 189404, 140865, 134948, 28936, 38460, 59707, 136053, 30880, 128067, 49530, 48855, 87894, 16331, 15771, 63989, + 58079, 104481, 125524, 14569, 128661, 25492, 365675, 116367, 126731, 94516, 122818, 30710, 67392, 52767, 2196, 47261, + 28051, 49914, 333288, 29945, 146885, 100058, 31013, 158363, 4861, 1817, 42266, 21215, 16216, 4256, 54248, 112813, + 97344, 128078, 30238, 120987, 42827, 6923, 14989, 69805, 147561, 47842, 51853, 2647, 153948, 13103, 39122, 18142, + 22684, 76687, 15882, 92285, 21335, 29519, 3993, 86408, 47685, 39612, 24929, 19453, 1853, 134405, 114177, 25894, + 43349, 26803, 12267, 92165, 15185, 61540, 9990, 69281, 59642, 76734, 309690, 136935, 10229, 92038, 49815, 104501, + 25520, 66774, 32406, 37445, 187921, 81418, 18633, 84262, 108972, 32019, 103853, 41207, 5579, 45804, 210683, 27613, + 98037, 39566, 18876, 154815, 24945, 108917, 31510, 38406, 6697, 20809, 29164, 106328, 19193, 8247, 16805, 3543, + 63734, 213048, 201574, 22433, 137934, 31798, 217223, 2939, 75056, 140267, 99972, 3047, 89740, 22878, 4763, 62402, + 19767, 110374, 49959, 24684, 224268, 106487, 32793, 8178, 56138, 27795, 3080, 77954, 63643, 24857, 121435, 175431, + 151661, 102435, 15023, 177670, 39313, 17174, 24416, 12895, 70618, 46646, 17001, 27902, 84031, 58519, 21749, 50823, + 89723, 59027, 57596, 61596, 84074, 33007, 8029, 24120, 13703, 108284, 63542, 58816, 85626, 83071, 91820, 14146, + 35460, 124390, 61351, 8006, 8867, 11495, 4529, 43870, 64845, 13482, 73015, 24763, 3439, 9485, 79856, 23851, + 57906, 220428, 88667, 80708, 99776, 38036, 39933, 208871, 63968, 30726, 291083, 68, 49270, 106842, 112123, 27384, + 81130, 110097, 118834, 241402, 34356, 13923, 23897, 40492, 16210, 71957, 62441, 58550, 23547, 13636, 20131, 42294, + 36446, 81802, 1100, 142364, 34090, 61710, 9270, 107601, 140028, 39980, 1414, 320109, 72439, 66107, 14862, 134653, + 2221, 1149, 9546, 36018, 22163, 35318, 143604, 19080, 57058, 48579, 2621, 55599, 363492, 110403, 14828, 57857, + 113754, 25759, 29811, 61553, 18913, 107232, 5290, 75792, 95451, 70056, 214553, 3329, 48663, 24095, 11961, 96108, + 54464, 155383, 53360, 112141, 54037, 49177, 57901, 67842, 176097, 123321, 6506, 228274, 68425, 4036, 160696, 23121, + 3023, 30678, 64279, 90792, 34906, 65080, 9259, 58549, 29482, 27140, 216012, 23499, 117389, 49482, 25665, 100543, + 341780, 54232, 60358, 235308, 80431, 37334, 14300, 53910, 58330, 29194, 117489, 59804, 16753, 37401, 37127, 35030, + 92616, 62680, 44495, 8116, 60907, 43835, 168603, 37896, 94846, 842, 40856, 25319, 147486, 395164, 90387, 68791, + 4498, 25599, 15543, 116574, 48646, 254235, 132631, 3917, 7773, 30355, 18277, 60008, 46801, 74243, 4222, 85032, + 7778, 17592, 14912, 22293, 18946, 6094, 46, 29454, 464978, 48886, 97248, 14694, 47558, 169023, 3388, 127473, + 33223, 22400, 144764, 181865, 177444, 13371, 44931, 27593, 7328, 194219, 91202, 3836, 15626, 22427, 52166, 39152, + 63337, 7531, 59378, 193696, 94700, 27634, 40257, 41337, 11743, 257393, 217307, 346548, 9351, 73104, 41502, 1488, + 255024, 105660, 39615, 20814, 39098, 149478, 69081, 19993, 16447, 55270, 37583, 19645, 42647, 14979, 8926, 28968, + 96230, 49277, 22527, 34250, 39769, 81745, 50791, 18698, 58840, 44616, 70138, 6720, 10068, 38140, 5653, 99473, + 63439, 3743, 19237, 163704, 35800, 1626, 33560, 38455, 65843, 158617, 28684, 92983, 58823, 71795, 71233, 1075, + 413844, 42288, 157276, 38514, 9156, 131335, 59762, 40948, 51258, 46584, 9950, 55371, 7434, 2577, 42703, 1693, + 61791, 27603, 63320, 25608, 85018, 30872, 100002, 36167, 6872, 2669, 51250, 778, 3692, 10451, 28383, 163025, + 28096, 44948, 19074, 128798, 7121, 36683, 2203, 17586, 33024, 70070, 348622, 5061, 6009, 23593, 42442, 28013, + 75532, 94062, 64585, 284254, 31997, 89645, 102394, 31393, 192535, 48721, 71088, 128192, 9661, 61738, 34411, 50069, + 3304, 16352, 53075, 45568, 9547, 42732, 1178, 93157, 14753, 88072, 51599, 88701, 31987, 23387, 63847, 44965, + 25314, 47565, 7560, 2438, 55689, 1314, 346, 23289, 15896, 475529, 112925, 131467, 20430, 150168, 2504, 17375, + 39472, 54601, 34817, 12000, 31340, 27414, 5063, 41639, 99744, 6404, 117189, 259172, 25398, 35063, 46527, 96170, + 115569, 8068, 179160, 161042, 54883, 97999, 36646, 8523, 28719, 11447, 6735, 26129, 205423, 83805, 44478, 94354, + 23071, 9474, 27662, 132536, 57855, 155315, 195915, 61922, 64638, 69412, 89700, 153852, 149867, 22483, 25631, 4401, + 25671, 191634, 58296, 7593, 82403, 23703, 17554, 61290, 37616, 211689, 4980, 2922, 20668, 148622, 109058, 2724, + 39989, 54579, 389750, 94744, 77996, 131928, 41416, 77516, 74948, 105981, 7862, 49124, 140555, 58696, 4033, 57560, + 175248, 201147, 43956, 80013, 64810, 82504, 14552, 11127, 36515, 10704, 23006, 45490, 46595, 111926, 16970, 31954, + 4958, 113746, 35379, 27153, 248773, 34760, 166030, 69750, 24045, 70012, 121173, 53304, 28728, 9870, 156097, 134089, + 136673, 71920, 25774, 2488, 168704, 5343, 127631, 74486, 20804, 188876, 26283, 102354, 114833, 476, 53497, 38795, + 100325, 26879, 18226, 1066, 27135, 41772, 14104, 58513, 21205, 5221, 84659, 49948, 96151, 18525, 149506, 51579, + 153134, 107909, 85993, 35590, 45992, 15182, 68394, 22750, 7093, 6602, 26954, 2528, 13992, 8645, 3748, 38754, + 76047, 16039, 28854, 52143, 1980, 22387, 6152, 255879, 19432, 56677, 64082, 99361, 145001, 56506, 42169, 13125, + 75159, 24500, 41901, 21053, 87462, 109469, 103771, 55888, 17710, 31989, 233429, 5318, 1013, 119131, 13220, 94790, + 45556, 27216, 5013, 108338, 34297, 51598, 16968, 224489, 144882, 29596, 70103, 32634, 20648, 23171, 115640, 2381, + 26061, 129018, 59090, 67066, 11319, 1052, 66080, 134106, 129567, 36464, 198632, 6394, 108555, 342064, 340, 57976, + 18872, 21980, 39272, 117475, 464580, 20395, 93823, 156783, 33386, 22005, 34188, 504700, 22717, 50887, 196433, 44491, + 65948, 106413, 3639, 94733, 167189, 37296, 49229, 1697, 5603, 70017, 72359, 61123, 135042, 93369, 6109, 45001, + 79542, 96019, 54203, 50884, 8801, 68912, 114197, 59072, 202632, 47922, 8431, 242124, 18114, 54405, 129410, 6472, + 91882, 124518, 39386, 91470, 5973, 31594, 93512, 401, 5239, 5661, 24933, 37492, 67315, 15503, 24586, 447, + 4431, 98481, 20358, 144946, 60916, 297453, 66825, 30645, 47819, 105167, 552, 87909, 71693, 40566, 5307, 32293, + 32597, 12315, 4634, 118577, 32606, 74622, 13999, 1446, 18183, 5010, 92389, 27675, 45072, 186756, 72549, 62625, + 80329, 3174, 188490, 17768, 76385, 56061, 44774, 4792, 24749, 6756, 29971, 24565, 51305, 2866, 185714, 7372, + 40314, 131257, 46345, 142745, 156514, 10853, 14992, 9306, 14693, 140671, 18567, 166507, 130345, 6503, 52141, 7521, + 13168, 8694, 14811, 40576, 66214, 114434, 97632, 88033, 18029, 21365, 15834, 397881, 12858, 6804, 73691, 171818, + 34801, 11558, 167427, 172844, 27628, 109803, 44373, 61609, 14544, 8723, 7897, 26839, 10823, 38501, 189122, 32876, + 40522, 18836, 231040, 28016, 40185, 9487, 60378, 40240, 33739, 35931, 69716, 16764, 148694, 148116, 26429, 90031, + 23548, 130862, 153367, 10154, 9923, 25899, 86890, 187712, 61012, 106844, 119164, 108121, 28859, 151900, 43746, 70054, + 17933, 46633, 32051, 40306, 19442, 73866, 51802, 202389, 34364, 59031, 39109, 86049, 99849, 27312, 354059, 431, + 164107, 160825, 29370, 26855, 141167, 209995, 47475, 25126, 30629, 112486, 16641, 31932, 21054, 13503, 62291, 8461, + 6744, 25340, 5056, 190589, 36491, 1498, 102273, 136482, 8096, 46702, 98246, 56502, 42474, 9181, 111985, 43767, + 41706, 30774, 3932, 26549, 155060, 66159, 102266, 53051, 30650, 208931, 3598, 31618, 10600, 67535, 135897, 87806, + 163442, 104978, 10409, 139772, 1143, 40979, 7330, 98219, 96655, 131263, 25023, 114039, 61390, 192001, 15973, 35549, + 52359, 902, 12202, 5580, 7559, 52829, 36364, 11107, 51568, 3787, 4394, 31819, 64256, 1505, 29813, 365608, + 203854, 33802, 39839, 47786, 4467, 50956, 226690, 12884, 22453, 47648, 16676, 45252, 14504, 2855, 18627, 541, + 436398, 14538, 2406, 20, 7878, 60282, 10602, 109448, 6980, 70267, 22616, 27176, 8293, 85130, 294480, 30144, + 63610, 187294, 289665, 163077, 293747, 55641, 995, 86282, 16167, 131142, 7732, 139426, 35763, 21669, 81048, 1053, + 19627, 16183, 153848, 41955, 147603, 49219, 127527, 60498, 15419, 62976, 59946, 18598, 18032, 16576, 207, 4670, + 110744, 11552, 9989, 2349, 51346, 15073, 25998, 160678, 33681, 220089, 68035, 65033, 54571, 77929, 12230, 88125, + 40472, 148399, 62247, 44687, 48615, 158618, 103484, 11572, 39073, 41233, 3610, 86331, 21604, 36776, 83989, 518, + 13754, 34617, 179678, 35290, 173027, 43237, 66547, 59016, 92560, 12741, 157332, 29334, 11083, 67849, 24492, 90041, + 47299, 109304, 10326, 20058, 63062, 46195, 31632, 9568, 11813, 949, 131768, 139099, 52007, 9458, 46429, 12293, + 29883, 97116, 3732, 32343, 9734, 20328, 4732, 83588, 139722, 11257, 49471, 2051, 15953, 233007, 15439, 88041, + 1550, 78033, 39910, 56576, 20651, 32790, 66091, 16869, 13616, 226368, 19098, 20124, 49306, 274210, 41089, 39818, + 16113, 202390, 49166, 5280, 90089, 148031, 55043, 2264, 92326, 62595, 168341, 67080, 7584, 39228, 2679, 31454, + 30712, 21771, 49469, 8092, 72424, 14892, 94819, 370101, 164858, 14108, 16628, 34424, 6831, 26672, 13360, 10293, + 152871, 13708, 152221, 56275, 55746, 3003, 189905, 73541, 197721, 19461, 138468, 38166, 34167, 86972, 78519, 126458, + 196442, 22647, 131900, 30322, 6022, 31039, 95120, 35519, 112107, 2704, 104049, 7805, 55215, 99039, 8898, 61822, + 7538, 79147, 8674, 19781, 123381, 122030, 61080, 29510, 4920, 252926, 24948, 29594, 43539, 79504, 36116, 27926, + 77165, 119791, 10396, 47075, 8939, 65089, 91291, 49470, 50392, 130812, 24665, 5396, 34192, 146915, 55, 32388, + 20225, 170176, 24246, 18217, 79762, 97481, 187002, 170504, 22505, 166717, 11581, 22954, 58667, 24092, 24239, 34967, + 40770, 168985, 20697, 10796, 29788, 36609, 33121, 48586, 97180, 70956, 4247, 10919, 82835, 29387, 24795, 134813, + 4568, 41932, 107494, 12409, 8579, 7615, 78083, 27482, 13273, 222151, 109832, 56337, 363569, 100711, 21692, 74289, + 35898, 156666, 112372, 33193, 49983, 165146, 13906, 30221, 436, 23307, 161876, 16834, 36598, 80261, 40181, 489, + 3237, 17307, 33708, 68069, 131691, 47411, 142213, 17996, 62418, 20656, 40859, 30297, 35591, 115572, 96762, 34638, + 8101, 100105, 87872, 93118, 4073, 13106, 53663, 14555, 379438, 12544, 34665, 144134, 65218, 83887, 41458, 1700, + 76072, 7062, 45362, 51519, 33887, 113928, 230002, 145590, 2968, 109731, 69584, 145887, 27573, 34080, 696, 54442, + 212619, 61698, 42014, 1469, 288680, 91524, 69494, 176890, 68278, 36380, 91390, 73061, 72851, 136365, 18061, 126629, + 150504, 108159, 73403, 20532, 217896, 18800, 83394, 3780, 6913, 42351, 72130, 124219, 121339, 338937, 19687, 8446, + 22017, 13873, 48885, 120125, 35340, 27891, 4562, 52291, 51072, 5972, 97159, 14055, 43616, 105781, 67483, 207916, + 75043, 12256, 28487, 7209, 31437, 59474, 13217, 149676, 10833, 46754, 7502, 32640, 81487, 26299, 56642, 3989, + 4364, 2409, 1896, 58704, 22968, 42546, 57069, 47889, 41454, 136134, 46051, 102015, 106687, 15526, 254717, 58, + 85446, 14369, 99446, 71688, 19863, 126847, 291582, 51244, 109625, 70818, 1547, 189380, 149241, 28615, 6289, 179303, + 524, 62440, 6853, 175754, 141850, 162709, 4217, 140213, 214404, 32835, 370939, 250072, 54376, 228761, 71916, 144701, + 657, 89940, 17521, 80160, 237023, 148575, 164257, 272527, 9401, 198903, 24729, 17703, 108137, 43135, 48966, 56162, + 53800, 36151, 13173, 1783, 32474, 18864, 70754, 46888, 49712, 30038, 58553, 64793, 53334, 174049, 42965, 84561, + 126876, 70090, 16520, 63753, 27337, 69921, 58122, 69010, 45552, 33142, 1092, 120910, 177696, 3676, 16059, 23396, + 8269, 22160, 9571, 34657, 15036, 46764, 37354, 25445, 12097, 63888, 48103, 145, 42240, 80858, 105547, 28234, + 2328, 51188, 12063, 12469, 125374, 98182, 171585, 129756, 119295, 23533, 25395, 181401, 99715, 107908, 42579, 37609, + 2500, 59133, 67194, 46635, 19624, 31959, 24153, 277972, 39441, 105587, 56371, 24069, 27220, 18122, 50693, 3846, + 102691, 55065, 140440, 293, 60957, 118436, 1340, 17314, 94543, 71522, 9010, 49481, 39101, 30757, 52442, 3349, + 18566, 55681, 6148, 49861, 67362, 29473, 16424, 51773, 13975, 16105, 153263, 53902, 78230, 197042, 15803, 187130, + 25017, 6214, 105388, 38599, 34017, 9107, 660, 114778, 239007, 212872, 16230, 195154, 90027, 38987, 248, 60897, + 39351, 34856, 31011, 21775, 41681, 1559, 85670, 6103, 35354, 83280, 187563, 5745, 43822, 13397, 20816, 140079, + 1043, 6348, 13019, 188905, 916, 83185, 13921, 197369, 58587, 308353, 44852, 37817, 141983, 32764, 68581, 40892, + 94818, 6526, 46289, 37353, 38799, 65245, 127045, 12280, 75459, 107508, 56307, 93576, 41114, 92631, 22742, 68224, + 67432, 122795, 2131, 30261, 16195, 71686, 80872, 19067, 36606, 55415, 51055, 65943, 59568, 48358, 40947, 230410, + 22272, 116297, 133612, 74166, 126769, 58783, 115647, 39171, 31424, 59980, 6420, 75687, 68659, 22219, 19662, 51609, + 12287, 7887, 94526, 61885, 134302, 46006, 92537, 80123, 257977, 126663, 55154, 71071, 5756, 38621, 29511, 61768, + 207285, 85526, 35878, 1517, 95637, 40711, 214057, 75041, 47248, 72951, 22699, 85378, 117689, 4729, 158936, 22518, + 19583, 25056, 17451, 43230, 77451, 141822, 2028, 7801, 22373, 4034, 75301, 60991, 12200, 59589, 123234, 17449, + 54993, 3264, 16430, 33128, 117118, 56124, 178609, 12642, 34244, 236200, 43665, 19313, 29386, 45091, 42098, 10042, + 34562, 71330, 29635, 50068, 53819, 124237, 44714, 32804, 71267, 130300, 48998, 56578, 64172, 172768, 50075, 17351, + 77665, 85602, 1594, 81728, 49368, 46606, 19775, 75183, 7716, 32889, 26648, 13436, 59301, 29561, 77044, 108652, + 25749, 26512, 343982, 16328, 45426, 53772, 84254, 67097, 194789, 61224, 17035, 160685, 17297, 202215, 135406, 118341, + 2650, 2712, 165122, 39668, 1766, 97847, 41583, 64750, 32501, 260547, 28864, 64103, 45198, 19516, 1158, 166912, + 20403, 34027, 10963, 16141, 20984, 163663, 185362, 27299, 6600, 243594, 45496, 154199, 14171, 53891, 52940, 101642, + 94604, 7963, 104592, 152606, 19037, 11118, 25808, 54515, 5402, 42084, 147184, 18390, 29896, 164225, 162873, 40466, + 9938, 54801, 70146, 66759, 59935, 43540, 58676, 69171, 109708, 38543, 32207, 46591, 88081, 20140, 41767, 101298, + 145182, 39899, 12204, 21085, 44844, 32313, 226062, 13138, 39167, 7649, 21294, 19544, 352626, 42947, 112978, 162137, + 164173, 121993, 17813, 6102, 35374, 5269, 42206, 30800, 45982, 22982, 36251, 17144, 6122, 8671, 8084, 272404, + 154, 122768, 12006, 76527, 73419, 69325, 105807, 9495, 220487, 29197, 89056, 160446, 53834, 197550, 37292, 117751, + 53601, 24091, 108269, 72650, 17992, 118251, 13578, 64227, 8609, 97876, 56750, 36113, 229321, 150223, 85160, 26383, + 5610, 88738, 33839, 35306, 68098, 12374, 121473, 27197, 66815, 63716, 10127, 10388, 71012, 155117, 10660, 38130, + 95069, 200906, 56997, 10546, 140968, 26164, 58789, 80414, 27396, 29337, 17319, 78747, 8957, 43718, 57739, 8704, + 134489, 9251, 14262, 40583, 24656, 39133, 5306, 43837, 86659, 164677, 194782, 27468, 56598, 41406, 95731, 17647, + 134852, 11972, 71605, 77846, 17316, 34195, 24465, 42471, 123838, 4286, 11465, 5223, 255436, 106016, 15363, 133653, + 6613, 57615, 21482, 5929, 41610, 5528, 159163, 20266, 138033, 2783, 48074, 249145, 81452, 57741, 38155, 31191, + 32023, 131830, 8712, 116513, 32396, 160702, 187621, 166002, 123687, 12, 62689, 145928, 63398, 18560, 86346, 150231, + 8693, 5478, 54663, 56869, 29712, 20471, 322015, 164692, 30407, 52016, 160121, 22929, 19296, 52881, 60340, 71650, + 121188, 31059, 10424, 72973, 3551, 30412, 44737, 172383, 36099, 243424, 5274, 49999, 20032, 79415, 43567, 95143, + 111948, 20318, 17729, 101737, 56624, 96891, 161576, 14956, 16547, 135980, 59262, 77152, 27453, 6123, 35571, 43380, + 35916, 62277, 21785, 53693, 15378, 108237, 63, 2276, 52039, 70272, 78694, 41537, 56849, 116796, 14411, 20761, + 13489, 233058, 9422, 23296, 22214, 27805, 167552, 26532, 73177, 43781, 1976, 47479, 53097, 70358, 25233, 10202, + 277349, 32720, 23465, 45782, 2157, 75011, 99414, 46797, 14029, 331188, 26634, 25912, 187886, 51411, 142415, 54672, + 10260, 67364, 68176, 84898, 141743, 32203, 8882, 16414, 246460, 67826, 1065, 38386, 91880, 168610, 5162, 41010, + 50869, 14162, 7962, 335266, 3788, 18011, 86185, 14140, 49486, 66814, 124474, 12893, 133566, 255655, 79151, 46849, + 54950, 40987, 113502, 4653, 33120, 1563, 160382, 117713, 129337, 309186, 18171, 10889, 53768, 44858, 38544, 36763, + 18333, 15858, 58971, 6477, 9525, 8535, 14726, 14096, 26902, 170756, 28405, 233366, 312251, 51708, 14127, 19199, + 10297, 110312, 48460, 646, 9020, 40769, 83604, 51716, 70759, 2649, 59125, 55621, 16647, 2952, 10961, 74126, + 112432, 43916, 267460, 5120, 59260, 28040, 31308, 16545, 84609, 47186, 40537, 205682, 9818, 19650, 93983, 42181, + 82766, 50191, 13339, 114720, 73569, 23501, 5541, 66254, 468, 17966, 5125, 81538, 46001, 88315, 134477, 4042, + 75780, 17161, 37372, 9273, 55028, 52868, 48506, 197660, 52106, 1678, 131509, 88997, 11498, 229161, 99808, 17550, + 43645, 124582, 219145, 8184, 108069, 70061, 175724, 99312, 17150, 2838, 7073, 156152, 17753, 49092, 16803, 1821, + 29417, 92090, 23379, 66219, 16705, 25405, 141529, 27280, 31799, 6767, 12496, 46640, 9606, 10300, 33865, 90498, + 289, 141972, 28645, 1755, 122254, 36574, 145200, 57778, 115975, 15433, 1941, 4099, 8620, 50560, 123303, 55676, + 6133, 5443, 25678, 28512, 255357, 14348, 122676, 93720, 56908, 9978, 32758, 60073, 14456, 30325, 74179, 182377, + 133464, 124701, 18020, 32177, 43554, 808, 19883, 16600, 79224, 7238, 18109, 28556, 11247, 50684, 94823, 7729, + 29630, 27895, 43494, 66615, 160, 75616, 204393, 4150, 12756, 120948, 108425, 9998, 25464, 61334, 213823, 15423, + 65960, 63934, 87262, 84230, 350428, 96963, 99319, 27630, 62521, 82558, 7456, 70035, 321796, 22677, 117013, 180582, + 100359, 79812, 34557, 287830, 67358, 14176, 80683, 114848, 35169, 90997, 1447, 22600, 46172, 146596, 10923, 103084, + 113128, 53346, 226456, 59683, 48988, 21632, 90741, 80771, 88868, 89090, 59673, 44207, 31094, 81602, 72782, 32997, + 33266, 124468, 127301, 33848, 6847, 2940, 167663, 1154, 60887, 4791, 68165, 51588, 98188, 27452, 53523, 3630, + 49659, 31844, 716, 23618, 69117, 101601, 4697, 29366, 92977, 133129, 100459, 35256, 220228, 220740, 11194, 50122, + 13947, 1305, 2379, 119210, 80181, 112061, 18955, 53969, 35103, 28242, 18281, 26482, 62170, 23125, 22627, 17903, + 97351, 70139, 14931, 69751, 13475, 194213, 6823, 66651, 2440, 3123, 124201, 127058, 199768, 273513, 29218, 168746, + 19498, 30628, 254726, 18151, 36597, 16458, 114447, 3813, 46971, 184066, 132731, 85793, 25234, 113561, 20977, 87033, + 67806, 81570, 82077, 83128, 62881, 16590, 59929, 31721, 84717, 54839, 152353, 27946, 73648, 1152, 51494, 25166, + 181966, 18536, 35859, 21096, 10488, 5434, 87296, 116782, 94149, 20100, 42748, 119284, 21550, 80954, 161142, 3281, + 26655, 56068, 31234, 68973, 63436, 197146, 77802, 53836, 48375, 31390, 138097, 215755, 14405, 14690, 48482, 192674, + 165650, 4356, 6779, 90318, 9621, 53563, 21892, 11380, 24439, 27988, 65408, 32100, 28043, 30121, 124, 52304, + 42735, 36882, 47875, 40915, 4490, 1857, 64523, 63890, 29963, 3265, 24732, 6558, 56674, 255187, 78937, 55716, + 45373, 202097, 105143, 40496, 1934, 50343, 10400, 93193, 262446, 123174, 33291, 88639, 50855, 19733, 11387, 78609, + 67098, 33565, 79076, 71724, 26898, 68956, 47175, 78105, 5261, 194162, 6861, 11334, 52696, 3195, 1099, 854, + 40644, 42446, 51986, 165826, 33900, 14512, 8567, 107082, 9440, 96468, 48368, 15017, 180286, 38407, 11266, 27073, + 87162, 25059, 1767, 90124, 22940, 50038, 4456, 79274, 19704, 269589, 3740, 24611, 26936, 118228, 122759, 44861, + 69769, 8268, 21928, 1448, 10254, 25662, 37572, 15808, 101759, 47818, 56338, 32066, 27406, 61598, 102489, 68037, + 12243, 45731, 6222, 13525, 48000, 97528, 22882, 28821, 73926, 12033, 35515, 19990, 113215, 45359, 13095, 69110, + 54935, 144153, 32952, 39972, 5726, 20322, 27148, 119607, 192787, 10814, 127655, 29129, 4312, 11899, 293735, 47721, + 106216, 47945, 13663, 4293, 9366, 4600, 36217, 51600, 11550, 30486, 35147, 4378, 52949, 225366, 876, 56535, + 23457, 10620, 14352, 63024, 212271, 53386, 55283, 2154, 277152, 6832, 58247, 34965, 133895, 60302, 8020, 17598, + 108374, 41827, 77422, 41356, 6191, 78382, 44389, 79737, 96477, 57997, 36253, 168231, 29980, 58643, 13506, 77777, + 218916, 163459, 37836, 70135, 58024, 40795, 89998, 95793, 54696, 46896, 3850, 14959, 40853, 50010, 53886, 103929, + 91124, 21842, 109259, 112031, 65894, 24294, 11400, 75618, 91170, 52085, 77528, 106068, 65908, 36186, 196059, 70011, + 252552, 674, 93814, 79169, 6793, 31343, 87518, 50063, 29212, 56507, 62602, 24490, 15389, 130371, 20806, 17839, + 44516, 4956, 102925, 118742, 122515, 17602, 47643, 17175, 52617, 34827, 384, 128737, 35058, 16456, 4055, 91444, + 9017, 27903, 32324, 74054, 103536, 349949, 23135, 91177, 39510, 20237, 139249, 107742, 49136, 161940, 10176, 4296, + 19242, 19236, 38664, 13941, 130652, 63883, 181786, 74033, 662077, 40517, 51656, 4092, 74699, 174254, 30240, 249851, + 47024, 124719, 88983, 17979, 31422, 88107, 12752, 18046, 8517, 112048, 15131, 61643, 73351, 4553, 10608, 181387, + 24399, 17507, 26238, 34094, 13867, 45419, 28560, 23320, 128360, 95692, 140246, 250559, 4810, 17968, 25372, 235183, + 4434, 11316, 6759, 113457, 61779, 50021, 20556, 133305, 111983, 259709, 231509, 141441, 61036, 58891, 28950, 14898, + 17798, 35773, 7261, 450465, 110240, 66004, 161650, 164984, 59722, 17874, 41866, 39325, 102960, 36234, 10606, 25254, + 39688, 16397, 879, 188946, 10001, 46267, 109745, 88992, 23803, 12899, 109186, 223568, 23039, 16254, 20592, 126376, + 176498, 68200, 93812, 5609, 56659, 71490, 16814, 75820, 44814, 26002, 31909, 11613, 134295, 51635, 17304, 5479, + 17188, 72639, 166564, 60617, 77577, 9173, 51736, 125261, 74466, 141449, 33396, 52135, 226175, 206041, 16540, 2241, + 102472, 15065, 11417, 44369, 154333, 39439, 21371, 35696, 63900, 86098, 215585, 10637, 111747, 26520, 35829, 5072, + 18062, 38762, 86113, 33683, 41171, 51676, 206735, 11386, 79669, 104994, 174586, 84969, 32773, 6701, 65682, 16472, + 408933, 62302, 88447, 143840, 42562, 29889, 168822, 199833, 28931, 31217, 94805, 6702, 30907, 53329, 73464, 80367, + 107388, 92999, 83741, 56375, 43487, 94239, 54863, 13740, 2946, 15038, 117251, 65511, 240310, 36372, 2795, 110090, + 23938, 154352, 180646, 13562, 24354, 38003, 14983, 27192, 319, 49724, 68544, 92943, 184983, 39339, 36199, 161825, + 7927, 16738, 7599, 1393, 6488, 53031, 27832, 35812, 1422, 77769, 52152, 9393, 10790, 70529, 103117, 58677, + 68809, 142754, 214789, 212425, 68209, 24340, 33236, 124155, 64775, 336, 120720, 43770, 4361, 63444, 9512, 52337, + 202, 37869, 58071, 28602, 17123, 124940, 64579, 79394, 59634, 16838, 71347, 33171, 51200, 72048, 194123, 84312, + 44391, 184338, 30592, 49986, 18188, 72135, 53498, 57477, 17843, 74498, 12560, 37524, 2619, 153428, 26875, 24918, + 74278, 49884, 44432, 39983, 3230, 39257, 81646, 26616, 9540, 23710, 69802, 52778, 47187, 280, 20102, 190963, + 21702, 33112, 201384, 189730, 36274, 151103, 62470, 79614, 56894, 160976, 37846, 3819, 43907, 28142, 33980, 44483, + 16310, 43780, 91255, 6410, 34790, 53414, 55594, 62493, 16866, 126630, 78730, 70800, 6150, 2638, 96447, 42805, + 5561, 80903, 142508, 69107, 13587, 90093, 68310, 13770, 107545, 142426, 6310, 11281, 108873, 30379, 19476, 19039, + 126867, 47619, 44321, 1557, 86986, 12174, 285300, 692, 28640, 174731, 39442, 33395, 33427, 183086, 62041, 33967, + 19017, 71946, 141533, 41962, 5762, 27368, 19966, 260045, 80637, 136704, 106076, 25336, 17430, 7907, 59393, 184, + 46903, 143058, 51209, 156531, 2047, 28617, 58028, 3727, 131055, 2181, 190078, 104219, 25958, 39516, 25800, 76861, + 13558, 64738, 31952, 28604, 5444, 142725, 28795, 87891, 47152, 833, 20563, 69475, 13900, 17091, 271888, 185043, + 44563, 4833, 59908, 40623, 122857, 138131, 6213, 136826, 45348, 94359, 56641, 22196, 70863, 57354, 56451, 72278, + 39593, 53647, 46234, 29708, 54332, 63721, 17639, 16420, 38068, 45645, 18971, 54437, 33637, 39722, 36484, 68634, + 318, 5298, 22418, 20417, 40310, 88, 18126, 44073, 143467, 137263, 71354, 82354, 18502, 189426, 11156, 72484, + 24520, 27572, 28397, 1057, 11377, 43227, 61610, 141001, 62013, 3621, 2123, 25838, 28942, 106389, 138280, 139177, + 27246, 10742, 1290, 24912, 28269, 40413, 12701, 122933, 83545, 3131, 5191, 33319, 17999, 208755, 75460, 44990, + 59015, 14954, 33696, 180654, 90707, 63223, 87538, 51, 30065, 70087, 47405, 49098, 15161, 490862, 57902, 10363, + 34720, 124013, 2826, 107593, 1263, 16539, 8297, 53595, 37008, 190173, 24783, 28381, 2012, 14817, 222228, 23569, + 6060, 37405, 4132, 23773, 98575, 114011, 35293, 24317, 92933, 80389, 14038, 96901, 5721, 58820, 71786, 38355, + 299, 2937, 128393, 129071, 199555, 22135, 61163, 3457, 24578, 103336, 75552, 8037, 29223, 24032, 36855, 65087, + 2985, 11252, 15167, 48922, 743, 16251, 113770, 51774, 115825, 202685, 4095, 133501, 109523, 3240, 22784, 51862, + 136657, 17899, 114978, 57429, 47454, 8657, 11392, 32391, 26378, 35272, 1426, 34467, 53586, 83481, 40561, 57729, + 3733, 111799, 328168, 6514, 174945, 20097, 14557, 18636, 93340, 171450, 639, 117760, 244456, 15998, 75359, 111774, + 5693, 73895, 98142, 34182, 37386, 132752, 48186, 121074, 28782, 11866, 26615, 23940, 89767, 129357, 80551, 82029, + 27545, 83711, 126798, 801, 23573, 21400, 128295, 14924, 18798, 114163, 50035, 114816, 136425, 471234, 15959, 173936, + 34320, 17327, 80636, 27686, 84778, 119579, 98823, 73515, 20041, 82828, 124250, 4650, 48453, 64519, 115563, 26853, + 38215, 37801, 92219, 69955, 7477, 145790, 19159, 94085, 71958, 65302, 12375, 44454, 40621, 106911, 19581, 3379, + 8773, 16999, 182583, 5202, 5874, 127304, 16993, 14116, 187927, 3375, 20370, 44171, 105965, 18978, 61953, 17115, + 51100, 102276, 75811, 7602, 43533, 31235, 7956, 72681, 18083, 5986, 190352, 3671, 8443, 19561, 18603, 95186, + 10180, 31524, 10515, 35607, 43597, 12356, 10299, 174108, 2003, 31154, 62144, 6234, 183999, 16214, 205583, 69997, + 69689, 1386, 87561, 18340, 12216, 23427, 2010, 44232, 129696, 140942, 7349, 4623, 146188, 5101, 86380, 150439, + 62389, 21860, 117536, 12248, 34044, 63481, 85500, 98463, 68410, 7339, 87770, 71963, 12765, 3686, 14919, 2974, + 43273, 7350, 39745, 6266, 26949, 192687, 75021, 968, 266807, 27515, 15493, 5904, 3345, 21226, 90343, 14616, + 34477, 13783, 5111, 69002, 79197, 20455, 25812, 125162, 5688, 23290, 86326, 151802, 47539, 53270, 120925, 57870, + 213110, 15305, 23776, 142238, 21634, 69658, 179702, 13601, 22257, 9455, 35397, 86555, 50092, 17185, 21662, 47115, + 32222, 159490, 66608, 20354, 42346, 75706, 11938, 55979, 39530, 138927, 7527, 13431, 63668, 92125, 206545, 83160, + 98, 105744, 113739, 10666, 134978, 88373, 50980, 17237, 74022, 5974, 44855, 31946, 5152, 17761, 22091, 89954, + 59088, 181724, 89377, 71648, 174145, 6081, 202459, 12825, 37220, 45669, 60029, 47529, 9934, 69759, 92928, 1003, + 9545, 40944, 40882, 123191, 118937, 215977, 4632, 152290, 5724, 38351, 20824, 19010, 87240, 135102, 56782, 135053, + 19875, 30902, 38714, 93406, 15784, 18212, 103460, 25829, 40143, 17780, 5626, 20039, 23263, 66779, 128772, 41751, + 87513, 216438, 5230, 73516, 181654, 37997, 80801, 90214, 285152, 76150, 31873, 8348, 37881, 138317, 50195, 1565, + 263241, 15964, 118491, 28092, 4966, 6035, 45147, 26418, 43934, 84355, 16241, 7487, 10433, 247295, 3172, 8129, + 186657, 57, 71773, 143295, 6470, 101381, 39489, 160086, 74416, 43233, 52957, 51944, 225854, 53358, 11933, 29452, + 25908, 40737, 49314, 60112, 142677, 7636, 42896, 27738, 246262, 17093, 14777, 56250, 32280, 129157, 16346, 76797, + 6192, 34415, 425, 120600, 75890, 191879, 176315, 63506, 45546, 161456, 5005, 46773, 143264, 38320, 150132, 134225, + 135305, 182762, 55889, 102851, 29742, 44842, 129661, 64244, 47013, 53257, 4250, 50419, 77787, 123983, 24915, 12948, + 11732, 36176, 80467, 160621, 126658, 56748, 175875, 78143, 8763, 54016, 205303, 6236, 37950, 84876, 66862, 80427, + 21806, 125486, 21484, 35813, 57557, 14539, 213401, 86192, 113464, 36625, 64405, 27231, 89465, 4451, 75847, 20978, + 108995, 205734, 68217, 94454, 164574, 18012, 255036, 16771, 23894, 158505, 7114, 43317, 22996, 11028, 52204, 124949, + 23169, 226500, 10370, 46407, 15369, 14412, 60558, 218161, 23117, 18847, 313212, 60955, 17642, 82698, 38578, 289214, + 130607, 42162, 81718, 82632, 40503, 951, 48442, 14289, 36239, 91499, 48742, 125633, 280990, 7266, 26286, 77911, + 44666, 7534, 217478, 178981, 9981, 2833, 22818, 156155, 40427, 12913, 72539, 44825, 147487, 28272, 67343, 16061, + 26869, 28878, 13104, 26717, 168452, 222284, 63772, 8001, 32886, 55288, 25367, 12083, 32991, 27965, 29014, 23535, + 46798, 8822, 7448, 101081, 240839, 93683, 48095, 16054, 15111, 14427, 104643, 135450, 70502, 37385, 89619, 135605, + 65697, 66256, 31643, 242955, 88548, 21883, 9676, 103291, 44145, 3863, 31735, 8400, 28701, 1387, 89573, 11921, + 48767, 27191, 47327, 74488, 31139, 34928, 58382, 10630, 206777, 28582, 17378, 118639, 35659, 45393, 41374, 26204, + 181164, 243974, 22596, 109998, 166262, 140883, 75323, 38999, 14554, 45944, 89326, 18593, 171445, 14273, 83848, 7094, + 31786, 136223, 135153, 75926, 66523, 5050, 82214, 24940, 76607, 13068, 103875, 30264, 17956, 28575, 70190, 14699, + 6507, 6918, 148803, 40975, 31279, 13140, 17326, 280841, 90476, 164678, 26191, 29026, 116611, 14717, 6030, 73654, + 167918, 94589, 13531, 31467, 6560, 37936, 764, 2646, 1243, 47040, 46211, 49422, 115324, 23197, 48193, 11038, + 80128, 4014, 18828, 39730, 41867, 964, 138962, 14313, 55897, 4976, 27379, 30682, 187323, 81139, 45324, 19782, + 37069, 15003, 3973, 32623, 32596, 5813, 218135, 46814, 189444, 1329, 15593, 67740, 145931, 8233, 95368, 52092, + 13390, 126973, 24773, 78080, 105530, 127257, 27684, 75829, 65709, 23804, 30679, 23341, 26805, 39433, 72773, 79105, + 6999, 9337, 78288, 91647, 55714, 45624, 31732, 25179, 41300, 62926, 8984, 56532, 22915, 82260, 13175, 111014, + 68951, 8391, 237398, 27237, 22138, 159504, 224263, 75273, 21120, 32545, 81951, 75664, 22264, 44392, 981, 6782, + 10058, 4181, 2250, 85033, 19945, 215931, 9376, 41673, 33635, 15417, 217394, 101669, 56123, 23340, 51752, 11920, + 99085, 5011, 143610, 229235, 10032, 59585, 16698, 27704, 5818, 10883, 13785, 186415, 6016, 52857, 9702, 70336, + 46649, 206034, 15092, 14481, 57476, 8081, 27610, 12151, 35264, 32218, 24641, 138702, 94413, 16922, 15037, 25736, + 112522, 11746, 14172, 11310, 262288, 112160, 142819, 50926, 93686, 24209, 43747, 11953, 83038, 1813, 102643, 324202, + 14341, 3919, 29176, 21127, 23204, 81844, 69984, 61119, 28807, 12474, 58355, 40271, 66084, 21889, 11758, 31845, + 77987, 65881, 45978, 68177, 6101, 28932, 58051, 649, 126673, 52123, 157370, 15105, 7133, 62360, 40724, 9837, + 38126, 27864, 30072, 264757, 5923, 6078, 20776, 4896, 122091, 30718, 48046, 119459, 170240, 303310, 26816, 100117, + 97772, 9974, 81454, 42024, 46874, 11564, 45132, 109732, 215746, 2127, 10903, 7713, 43948, 4937, 28852, 25103, + 41622, 38117, 17887, 60135, 3272, 72498, 31571, 43132, 55596, 108898, 45911, 110563, 8332, 37358, 183144, 1744, + 146411, 106155, 85432, 89589, 251315, 29773, 4572, 57991, 13533, 23984, 36596, 74746, 8561, 47865, 143388, 13408, + 81521, 143096, 93820, 10893, 115449, 113660, 48899, 7902, 48616, 6164, 68386, 80304, 175175, 147319, 43500, 47779, + 2063, 16353, 18616, 12432, 186556, 23124, 95665, 69513, 3036, 14556, 14786, 10437, 134537, 36883, 56269, 63535, + 75772, 100719, 86026, 42447, 29728, 3767, 25145, 40239, 82360, 26124, 91863, 12060, 22973, 30854, 96321, 53650, + 186559, 22801, 8489, 72885, 86348, 51954, 28230, 88192, 89100, 269995, 13885, 51315, 38388, 73083, 25625, 53485, + 82297, 39389, 100926, 72363, 45610, 10521, 13154, 68652, 2613, 44579, 170934, 38080, 87082, 32745, 40511, 28882, + 9986, 23752, 68927, 62035, 177812, 181149, 29031, 11611, 57884, 182442, 8046, 104980, 23591, 100153, 104125, 9117, + 47485, 23873, 2671, 349983, 42543, 328134, 85104, 58966, 33582, 332001, 133483, 9354, 44713, 26316, 6446, 63766, + 74439, 40756, 76029, 97107, 257444, 43586, 84500, 59959, 252451, 55620, 150696, 63676, 31825, 65735, 146929, 23371, + 35631, 35977, 145121, 51984, 38540, 33976, 24513, 207079, 33066, 10465, 7127, 153150, 5147, 36952, 154507, 3865, + 13973, 14200, 52272, 11308, 4343, 15766, 13965, 24679, 51830, 184838, 3348, 86524, 70378, 36337, 84987, 49030, + 22827, 32995, 19326, 2046, 26448, 253830, 60248, 12393, 95560, 44044, 28370, 1662, 36896, 50220, 48315, 80320, + 241741, 43652, 242555, 131179, 48067, 39495, 113599, 13797, 203953, 20287, 78696, 3410, 298860, 46405, 39410, 64369, + 61620, 171971, 71030, 204186, 20450, 29322, 37991, 260572, 3220, 386508, 87523, 9404, 67272, 73458, 10375, 45255, + 6586, 2590, 34096, 4160, 107662, 57683, 97396, 79188, 100160, 35851, 78921, 149875, 108684, 200141, 33908, 53318, + 6929, 19857, 56702, 3398, 57226, 58810, 9304, 20429, 4762, 64257, 64571, 51955, 7457, 60202, 39068, 65191, + 1320, 89495, 11353, 17456, 40404, 104230, 19164, 17854, 77204, 58530, 172392, 75503, 99309, 15916, 157308, 83740, + 62750, 50622, 1879, 15474, 208653, 18824, 11343, 41248, 59977, 127748, 31363, 172064, 44000, 65018, 12188, 41891, + 74315, 17651, 19590, 90710, 34332, 9615, 58267, 127126, 5819, 63902, 44975, 20415, 172217, 26030, 99297, 158027, + 64904, 15382, 45953, 118417, 114077, 18724, 56092, 87313, 18147, 79997, 136198, 62361, 84012, 22885, 9665, 4621, + 1791, 3009, 54017, 91348, 98456, 56262, 72712, 106254, 90930, 42901, 80747, 25508, 21446, 133798, 113357, 6097, + 116669, 1181, 110413, 11032, 103938, 49121, 260341, 161282, 7422, 24145, 56140, 35654, 85140, 174230, 9633, 104905, + 59713, 728, 60193, 191876, 5768, 22655, 5145, 41262, 326211, 147566, 80079, 41245, 16239, 59176, 15547, 123829, + 75411, 13376, 315047, 105840, 13229, 35046, 43694, 56413, 29398, 90069, 53794, 84673, 10758, 107725, 5524, 23780, + 236107, 388309, 62023, 165588, 1539, 46003, 176003, 163955, 112472, 361654, 29424, 49364, 95979, 3700, 306600, 117453, + 152154, 17800, 82564, 14444, 151294, 22058, 29517, 47312, 306, 266768, 196797, 94605, 21196, 107639, 225607, 18057, + 38146, 50176, 69453, 50095, 10700, 216046, 17364, 47494, 6891, 29894, 48715, 14004, 84282, 21694, 7598, 82070, + 109646, 6365, 16302, 27108, 56492, 142883, 77880, 27851, 40539, 187868, 189893, 289432, 6589, 19096, 22176, 166724, + 119491, 38469, 38709, 163079, 51354, 26677, 199471, 115939, 30685, 126480, 79686, 66788, 140209, 95841, 256423, 20274, + 136906, 108937, 4472, 99520, 29622, 157862, 29670, 35606, 73617, 56291, 14416, 1391, 49553, 41902, 66050, 23269, + 70525, 139634, 148637, 11479, 51671, 3128, 65679, 40966, 166869, 116434, 159850, 7654, 139616, 20315, 65982, 116183, + 74395, 50212, 88368, 27581, 37439, 11453, 97247, 212239, 49595, 3922, 25404, 51622, 45678, 120847, 23534, 2190, + 11959, 15866, 21030, 7156, 33211, 32273, 16756, 51864, 86560, 62359, 37272, 150553, 52434, 48096, 52877, 35909, + 9282, 150331, 56064, 3339, 62690, 77469, 38848, 312832, 112155, 50347, 133337, 6119, 130810, 19939, 40188, 198954, + 5243, 178898, 39868, 142856, 108261, 286939, 44549, 159984, 99970, 197697, 81046, 134326, 265613, 8809, 13626, 21584, + 72551, 29643, 102979, 213474, 80049, 198207, 20362, 229516, 6391, 82595, 72275, 12563, 33365, 2420, 161399, 254521, + 90721, 10070, 61781, 32490, 66737, 212773, 229338, 7775, 69872, 54551, 80069, 13914, 87011, 91386, 134664, 33101, + 1860, 15322, 69366, 97910, 9032, 31405, 11616, 221, 112544, 23414, 109925, 66229, 60905, 34215, 18312, 31402, + 37371, 77552, 57720, 2026, 89015, 4380, 50369, 20157, 140351, 42001, 57692, 30433, 19076, 51739, 23715, 62058, + 850, 121732, 145992, 46915, 373531, 25804, 8590, 87747, 2802, 16807, 15221, 116280, 36725, 12360, 34724, 117090, + 218795, 142043, 148440, 65614, 72062, 18466, 55923, 22439, 28990, 58866, 64866, 114538, 16550, 89174, 112318, 27549, + 24614, 155152, 5486, 45048, 7815, 58664, 6423, 11415, 6187, 21207, 67086, 238124, 26336, 2489, 21350, 54052, + 33373, 60539, 51387, 100319, 32162, 11584, 95109, 44016, 42791, 31049, 47206, 52852, 73555, 110693, 7535, 38410, + 32062, 15667, 9670, 65566, 23386, 531, 44985, 2760, 10244, 123017, 50775, 39638, 56392, 170971, 54953, 18366, + 49442, 134359, 57768, 10659, 27076, 77194, 62382, 113419, 136262, 150169, 22322, 207134, 12412, 139797, 55514, 2505, + 14883, 65500, 22972, 15267, 1134, 64278, 37799, 235955, 33675, 43711, 22813, 276041, 97153, 48116, 34495, 6178, + 199281, 32510, 95181, 5794, 15608, 76263, 19924, 230629, 100152, 10562, 76444, 119798, 74072, 219457, 36986, 12066, + 47942, 54591, 35202, 23051, 254301, 155103, 68248, 13470, 36451, 42899, 93606, 121040, 16026, 27968, 10851, 17794, + 10687, 100974, 49021, 10866, 65067, 10018, 39088, 10965, 56708, 897, 11410, 7452, 254030, 47692, 32629, 18771, + 30290, 48037, 43471, 14347, 50490, 66808, 37049, 49968, 13864, 83559, 25801, 3591, 57941, 75692, 173303, 61385, + 259331, 1969, 57685, 2094, 35588, 6233, 27697, 16717, 23485, 26772, 4734, 15135, 43486, 85019, 26988, 179071, + 24869, 25026, 9295, 27083, 21620, 11383, 45847, 134822, 92971, 19856, 42005, 31000, 22072, 2896, 21798, 125082, + 88645, 561, 47297, 28868, 1048, 75739, 25425, 197147, 182050, 124782, 126886, 12162, 13343, 152665, 53046, 7557, + 32452, 9893, 110355, 9538, 14825, 62686, 7879, 104424, 19509, 31568, 4996, 5559, 3325, 22164, 66618, 2476, + 216938, 38862, 52182, 79198, 45740, 52776, 32070, 132672, 99716, 19543, 5515, 40777, 189082, 6051, 3103, 146615, + 53740, 256827, 80531, 104166, 78245, 34550, 28933, 112044, 25609, 72638, 36640, 25629, 24311, 56326, 11524, 83163, + 176777, 23393, 82414, 6106, 47340, 19377, 61707, 10698, 308354, 82475, 8066, 15310, 40669, 62347, 33738, 15955, + 66085, 140789, 4852, 37500, 14102, 5845, 9813, 54656, 125339, 67825, 97677, 67735, 9225, 11506, 173536, 159289, + 128709, 12613, 20379, 46259, 97207, 42699, 91068, 45947, 1271, 211146, 104284, 55003, 200933, 14250, 55082, 49995, + 78439, 185897, 62876, 11600, 113451, 32229, 199030, 36486, 88975, 65343, 140167, 135960, 18324, 638, 86929, 96115, + 46521, 34134, 437, 7115, 11819, 80629, 96102, 12424, 18570, 81183, 15089, 30525, 141756, 201210, 66036, 47056, + 72512, 98759, 18003, 68671, 170020, 14775, 7872, 86707, 52754, 279230, 82966, 13276, 63550, 101747, 103537, 30259, + 118515, 110652, 15079, 51435, 103073, 104977, 76964, 5981, 93330, 91388, 21050, 56718, 32736, 2464, 36579, 80299, + 50499, 49852, 67313, 130037, 14722, 2418, 7783, 76521, 31600, 78508, 133834, 49167, 68452, 47680, 2363, 25459, + 398867, 67795, 165159, 68999, 29316, 33111, 23239, 12957, 172786, 66330, 3816, 4414, 18417, 12030, 30134, 7919, + 104924, 9960, 36133, 26144, 2606, 105224, 32252, 42036, 5670, 72687, 493, 78524, 84818, 34715, 26322, 28439, + 16288, 21908, 74255, 9962, 67106, 147542, 139191, 43764, 59580, 72920, 393509, 63136, 82929, 53980, 78657, 4543, + 607401, 11665, 318088, 11366, 291, 7537, 212378, 77254, 85829, 59252, 37336, 13232, 359, 43117, 65592, 71269, + 15897, 112396, 53939, 40125, 35830, 56176, 59326, 11017, 50696, 114234, 276483, 22837, 65630, 17802, 22227, 18232, + 52672, 51170, 100713, 92360, 22115, 91842, 43063, 195957, 356968, 3794, 166425, 56044, 29895, 163395, 11168, 56699, + 40837, 67702, 27339, 20360, 231192, 89936, 103744, 1998, 34024, 32020, 3803, 117654, 38957, 94943, 70290, 85606, + 26722, 43088, 170484, 36210, 406, 282841, 54770, 175134, 23335, 44094, 73528, 47037, 124952, 31360, 23208, 78534, + 72068, 123285, 11398, 40458, 68804, 30009, 6939, 3499, 13268, 40221, 12223, 61566, 147101, 333845, 73905, 2372, + 164740, 293468, 55614, 327574, 276569, 59394, 21940, 154180, 162596, 28918, 37039, 166169, 66943, 84556, 40144, 10616, + 11569, 25337, 104847, 48420, 26654, 76526, 228642, 20116, 66358, 44381, 25600, 2578, 4777, 70479, 5757, 64766, + 23229, 11688, 27998, 24560, 102127, 6006, 130766, 11689, 5848, 24290, 203474, 51926, 978, 76149, 170663, 68953, + 2921, 5461, 117041, 24360, 59666, 1098, 64926, 198078, 5371, 1164, 166512, 13456, 28212, 22987, 95713, 13302, + 90108, 31433, 120078, 63947, 42938, 68482, 38260, 42265, 39320, 109797, 110494, 79743, 2499, 2553, 58577, 180281, + 4271, 259624, 94417, 68375, 108792, 50431, 9717, 29255, 33510, 160264, 7272, 343301, 125072, 154624, 6168, 27338, + 71653, 51148, 140929, 51394, 65239, 109678, 179395, 7761, 38250, 81439, 23490, 79048, 66357, 53948, 107018, 28855, + 38577, 94122, 43589, 44430, 13964, 103761, 2708, 12411, 86251, 119198, 17302, 51623, 35708, 305, 95393, 8798, + 50755, 41461, 203637, 19736, 36010, 8599, 54546, 13603, 29448, 118755, 50260, 10357, 12209, 86678, 39594, 88467, + 3844, 173096, 17788, 39975, 38222, 14809, 54370, 53581, 206337, 67848, 23694, 2309, 100876, 41983, 276960, 18075, + 67827, 14170, 117970, 89349, 137088, 75893, 70548, 20757, 14167, 10804, 5959, 67463, 252225, 44451, 87528, 36335, + 84163, 175996, 66912, 69227, 195270, 25238, 167523, 96366, 1306, 7967, 27706, 52700, 5703, 285, 51677, 60197, + 54198, 170697, 20548, 18244, 779, 4822, 39984, 71212, 46802, 72502, 31290, 74896, 22028, 154697, 58236, 131173, + 51124, 252252, 64234, 48608, 86759, 36236, 13170, 143379, 70560, 101041, 195793, 70671, 113164, 99377, 70248, 34118, + 35685, 116394, 50149, 302730, 162145, 121592, 530, 30881, 45471, 162432, 6235, 49645, 34561, 40287, 58509, 43757, + 422, 70918, 113036, 190344, 2611, 233661, 162936, 32114, 6464, 94933, 54217, 64327, 47486, 871, 90931, 33404, + 19223, 20183, 3928, 34508, 38246, 36359, 11459, 66339, 9191, 90968, 122115, 45027, 18331, 84569, 82055, 106565, + 89942, 52285, 40019, 20438, 243642, 100401, 166242, 127119, 212364, 42312, 34711, 1671, 15893, 23179, 5020, 74061, + 17518, 110465, 11940, 3873, 22617, 123195, 18144, 100726, 6409, 91356, 45936, 73471, 30046, 108852, 212969, 66765, + 126182, 98830, 107226, 23993, 59716, 48049, 45651, 82888, 36560, 16256, 52004, 17296, 104428, 12933, 38645, 135609, + 18846, 26099, 40801, 56830, 26592, 992, 156526, 79480, 19458, 91618, 39463, 7988, 50793, 54675, 156601, 19881, + 147333, 1159, 50024, 77736, 30826, 64647, 13710, 115978, 1388, 51510, 5276, 207487, 27647, 59310, 5123, 271841, + 10922, 2382, 11425, 17267, 14495, 244507, 2126, 492, 33545, 12138, 8818, 184454, 19269, 134769, 8528, 57017, + 135828, 73552, 22221, 65808, 39727, 367870, 203492, 24483, 41601, 196988, 198, 55446, 46931, 68675, 244761, 5411, + 233379, 19207, 36423, 316277, 49169, 745, 204311, 317017, 131130, 150130, 101903, 260111, 182112, 30434, 25375, 59274, + 16276, 109977, 54255, 20999, 82381, 135770, 2885, 31724, 118209, 21645, 119343, 36886, 142445, 81249, 42421, 43503, + 128310, 66260, 92555, 94890, 19672, 1769, 178045, 35419, 28740, 2136, 226543, 24030, 82907, 124857, 54353, 157870, + 33436, 38109, 85642, 96673, 3118, 112407, 1944, 31498, 102206, 135319, 205619, 160787, 28723, 91910, 50034, 79540, + 24819, 28372, 80113, 173951, 41937, 15370, 19059, 55603, 38854, 100638, 70561, 519, 5157, 19218, 16617, 91793, + 3881, 75012, 176191, 145596, 111491, 20452, 154738, 27981, 1142, 2054, 22256, 54130, 9776, 19737, 32399, 69945, + 421673, 103058, 91031, 7281, 152241, 74595, 46116, 86993, 29309, 22846, 33982, 54529, 14961, 41775, 23014, 131668, + 87854, 171036, 94711, 50319, 6054, 72531, 3482, 3581, 15424, 83151, 45387, 66155, 3796, 118067, 32026, 181774, + 82656, 49811, 12569, 44671, 54996, 83240, 157346, 143069, 2108, 19813, 11164, 42601, 55367, 1359, 101577, 27699, + 239450, 9023, 33206, 152235, 154525, 73472, 7296, 55929, 9643, 80206, 87554, 68722, 118103, 89632, 161537, 59640, + 106041, 77231, 63719, 12373, 64601, 98305, 1056, 46674, 68549, 18960, 17748, 19013, 48707, 296146, 134285, 64092, + 30266, 15379, 85084, 87899, 25772, 62788, 25525, 31250, 18740, 80665, 23101, 34025, 9462, 7075, 49746, 39284, + 229669, 57834, 2626, 248569, 91798, 873, 22206, 84442, 112152, 160148, 59240, 6711, 191327, 15256, 141511, 171566, + 14493, 68797, 15010, 17086, 72828, 164513, 36088, 32054, 8175, 11054, 81290, 64307, 66636, 51647, 21137, 68255, + 236474, 72999, 12123, 66901, 25817, 58290, 23813, 41818, 87351, 51685, 349139, 15386, 129027, 92193, 14750, 7028, + 76653, 56861, 59524, 43395, 20422, 123741, 40958, 19478, 22983, 87931, 5921, 15341, 71240, 18213, 18961, 25648, + 27846, 61261, 75568, 216919, 44661, 12442, 49311, 68342, 12399, 74324, 7455, 42754, 46158, 66251, 405, 72411, + 77704, 58295, 15625, 4552, 53101, 50537, 30941, 37141, 35032, 18292, 98289, 17870, 11072, 115848, 60108, 70972, + 17300, 13269, 63524, 140693, 109294, 93883, 56701, 69184, 33638, 4485, 36667, 26721, 24408, 5954, 28290, 80247, + 1895, 82128, 40307, 96015, 11241, 5825, 45230, 255638, 760, 31698, 12512, 26145, 17584, 92444, 8948, 17954, + 82479, 9085, 5850, 120208, 125877, 9751, 11265, 22102, 63150, 153550, 69826, 75885, 141075, 131001, 14419, 128804, + 34259, 129918, 115229, 23808, 23274, 3580, 82265, 18942, 81698, 8545, 39913, 79933, 15732, 6741, 38339, 39271, + 43577, 31006, 30604, 53478, 48340, 102062, 39630, 12695, 91584, 222, 20589, 89230, 14688, 30824, 97582, 47266, + 16379, 99608, 42679, 70464, 24481, 4475, 80121, 49522, 150280, 121584, 178585, 20071, 96420, 5695, 31648, 64033, + 262050, 20662, 107571, 34749, 48635, 192388, 60052, 163993, 43727, 40545, 72642, 99324, 61819, 17935, 20846, 61496, + 56268, 69226, 133071, 52853, 72003, 57628, 110499, 29460, 88178, 40245, 24970, 58958, 17281, 21360, 121825, 31853, + 79912, 81792, 201844, 95444, 13218, 256154, 26236, 61260, 122519, 90685, 37984, 5119, 125295, 126359, 310134, 54407, + 166396, 6520, 28971, 31149, 11811, 266489, 27120, 1794, 2171, 23105, 744, 2814, 118930, 46693, 140092, 4993, + 67746, 27308, 66270, 97039, 17636, 6061, 69135, 4202, 178278, 7472, 32642, 40673, 174656, 26758, 204108, 44815, + 95661, 95589, 192828, 73663, 173039, 77882, 43232, 71654, 83845, 55846, 26313, 21216, 79689, 31469, 85659, 11793, + 17473, 17000, 64471, 78858, 98555, 104223, 20905, 121028, 127696, 15679, 22246, 93167, 203415, 40670, 1525, 47197, + 54730, 29955, 27650, 142614, 22925, 38365, 107626, 61283, 232239, 25514, 194946, 12768, 9309, 63949, 114873, 57567, + 12136, 30868, 3548, 537341, 175026, 133711, 27455, 27667, 20740, 32351, 1997, 26211, 180188, 35259, 10358, 54362, + 10747, 42370, 12304, 6425, 39816, 22704, 99010, 215128, 314017, 17879, 58536, 20732, 266131, 43327, 1650, 27592, + 10040, 89403, 28410, 125002, 175732, 21475, 13832, 98954, 112550, 155503, 53781, 62057, 220651, 63490, 218647, 26496, + 31974, 28320, 13557, 72935, 37393, 40244, 102949, 25746, 888, 15552, 12165, 23782, 23008, 37306, 182690, 178294, + 86799, 19876, 69717, 10583, 4303, 116880, 7218, 92683, 64905, 100026, 340736, 142052, 148467, 8925, 2702, 63925, + 75337, 81983, 220124, 89751, 251, 226035, 14097, 1808, 3284, 142418, 16036, 72819, 370102, 13289, 144922, 3996, + 50264, 199033, 45199, 139880, 9835, 4702, 60405, 74816, 5438, 7368, 27687, 162954, 23655, 159039, 21280, 61851, + 4481, 92865, 109762, 3285, 29851, 3021, 104939, 2905, 329, 63385, 22681, 52094, 12855, 38488, 18381, 19211, + 7162, 61266, 8835, 22825, 64931, 45593, 66502, 25309, 78141, 46199, 59413, 50610, 12804, 59952, 186517, 61018, + 42372, 46728, 18388, 90815, 296771, 59091, 46636, 192289, 83547, 3423, 29852, 2745, 18624, 16583, 357641, 32404, + 34874, 30511, 86377, 868, 86271, 59760, 81404, 39749, 3360, 74207, 15394, 156217, 48665, 41137, 72366, 52831, + 77735, 59042, 22515, 6142, 88767, 22116, 68286, 40920, 11463, 78197, 68958, 24062, 63527, 100286, 139882, 65777, + 28889, 12481, 28953, 8266, 22258, 3319, 99181, 17609, 29140, 179534, 30832, 42841, 194315, 120705, 27548, 161124, + 113924, 42548, 41864, 56260, 25499, 42783, 177062, 105955, 6406, 14311, 23992, 86657, 31334, 225197, 24185, 39921, + 1845, 104026, 301294, 95718, 4802, 8899, 157667, 77564, 49184, 6115, 80340, 47518, 43455, 6339, 54561, 39882, + 35469, 115497, 123233, 68548, 127594, 20262, 97680, 60841, 92970, 5781, 28954, 4558, 61038, 45382, 35089, 49876, + 115005, 15489, 27010, 91676, 38840, 12352, 20606, 19800, 87761, 12264, 9268, 146639, 106838, 47766, 91230, 8234, + 8811, 48534, 107720, 27259, 20572, 34400, 108143, 52933, 55637, 28872, 61739, 77203, 11162, 21038, 66975, 30423, + 96721, 31993, 45541, 7376, 132425, 71889, 178420, 446221, 108925, 260438, 102283, 4056, 2948, 77259, 83943, 38199, + 125457, 36830, 123208, 391, 36356, 138390, 99456, 92051, 3502, 239674, 36201, 114068, 75270, 3160, 39536, 218269, + 27622, 12173, 56780, 8501, 127192, 66434, 47097, 13635, 2561, 98519, 73258, 96646, 123095, 5710, 42788, 66384, + 49394, 12035, 7389, 23253, 61155, 251141, 4195, 439, 16897, 56354, 25580, 66462, 110064, 188570, 17260, 12827, + 9699, 13844, 208611, 7653, 89448, 41275, 5078, 37917, 53356, 45195, 15877, 74097, 19628, 231041, 21225, 15175, + 220310, 3514, 79626, 97496, 21622, 20434, 48926, 95346, 83036, 47481, 10584, 14331, 9885, 4023, 29396, 21139, + 112214, 87100, 83793, 9796, 6087, 423, 60612, 11748, 26713, 29951, 132442, 40260, 17901, 55713, 5620, 88019, + 161912, 177970, 3729, 49808, 91492, 35869, 138357, 40508, 3440, 61216, 56765, 68562, 68594, 2747, 88777, 43463, + 9266, 44125, 1567, 2354, 92238, 29774, 47207, 47789, 8087, 20375, 191924, 3415, 6866, 22316, 82861, 233038, + 150194, 13698, 143688, 29411, 72175, 16465, 14358, 220015, 80701, 53366, 59020, 22661, 13459, 20745, 8739, 76074, + 31836, 46743, 45518, 51271, 43243, 19787, 114669, 18136, 239700, 15692, 105609, 60536, 95846, 27460, 7762, 225232, + 44749, 11206, 14819, 1690, 50647, 170657, 224611, 139596, 21945, 134017, 15972, 174955, 230538, 2804, 25876, 121127, + 120612, 18921, 14091, 435, 132371, 178953, 144326, 158152, 244604, 220898, 21478, 121856, 5193, 4031, 105823, 11008, + 105637, 134379, 253591, 97747, 34661, 247232, 20987, 6949, 41341, 106816, 110210, 45958, 68775, 150399, 11104, 93886, + 85393, 28015, 147749, 112829, 1874, 19994, 21402, 16367, 8771, 33037, 11041, 96701, 33718, 36354, 26705, 23369, + 49672, 29673, 72422, 32419, 77403, 36496, 28454, 23255, 595452, 242129, 61562, 58092, 99507, 41978, 40275, 32822, + 6490, 1688, 175006, 8864, 58895, 13716, 45499, 120546, 128742, 24764, 141091, 121483, 7704, 83412, 14149, 58968, + 39239, 165272, 32855, 72184, 73217, 52628, 13081, 73279, 43816, 9383, 216195, 56823, 62824, 48448, 191659, 3540, + 37804, 223316, 171995, 17606, 199976, 21733, 141024, 23939, 22361, 42786, 77686, 3523, 80005, 1542, 22284, 32365, + 87514, 43833, 4665, 93155, 94832, 32683, 134693, 9494, 14089, 54921, 16128, 131782, 4574, 168587, 76247, 7989, + 139975, 821, 8368, 108503, 59142, 158797, 137, 205170, 75523, 18074, 13682, 91077, 100268, 65492, 54879, 15629, + 43906, 38056, 45569, 40180, 53442, 24989, 20763, 24867, 15152, 30094, 129619, 140074, 2547, 23241, 27435, 7171, + 186002, 4003, 5665, 192737, 17011, 57494, 230276, 241405, 19513, 27773, 95035, 92634, 204282, 5213, 32107, 87507, + 3343, 10550, 3806, 71001, 60568, 10837, 23329, 144168, 128318, 1900, 47551, 4240, 119250, 50444, 64351, 85851, + 4298, 169567, 1401, 13814, 51871, 3524, 75657, 25885, 41336, 136110, 12759, 77034, 71759, 22871, 604, 13904, + 21921, 84968, 84920, 208954, 45074, 13960, 4204, 102255, 98169, 58850, 58448, 58879, 145889, 22357, 8919, 58428, + 99427, 13803, 157733, 68068, 11350, 61811, 360594, 118202, 1237, 824, 163104, 118356, 5520, 769, 31581, 20685, + 28799, 181670, 40637, 38360, 7803, 8532, 69133, 37235, 53702, 86519, 85294, 62552, 21026, 8827, 142049, 30386, + 136352, 11344, 158995, 19682, 38293, 242831, 103750, 55804, 128690, 108982, 27181, 18409, 12158, 167408, 120214, 132169, + 90132, 134213, 7909, 28749, 44600, 10115, 55121, 16581, 10184, 82321, 25270, 21542, 26957, 2707, 106897, 145041, + 39459, 145473, 48977, 26927, 126025, 157588, 249490, 64382, 78904, 11519, 1284, 9871, 82999, 78364, 173378, 109477, + 59373, 50500, 2168, 30838, 39301, 154212, 66143, 91333, 150198, 28707, 45440, 20859, 120529, 33550, 21869, 80014, + 153042, 19905, 153475, 81658, 20177, 158807, 120156, 38566, 50089, 6373, 63762, 19510, 14764, 26971, 108976, 72526, + 271571, 84066, 18309, 66438, 30530, 98093, 65740, 53411, 123161, 23236, 24050, 64130, 38975, 177329, 37078, 133183, + 101562, 89382, 51844, 19732, 22941, 26188, 51520, 22735, 5648, 43118, 130081, 12788, 124654, 200339, 25097, 48211, + 109243, 196680, 216387, 69966, 69817, 55482, 6031, 5293, 71675, 18384, 137078, 73066, 49162, 68808, 11413, 25901, + 106884, 643, 4412, 18355, 21241, 36413, 7382, 16629, 107795, 6893, 5332, 242, 30258, 49533, 74544, 39490, + 16572, 4199, 12724, 122748, 188262, 108611, 126989, 88570, 141456, 72114, 87870, 20276, 7688, 37800, 22712, 59241, + 60718, 170557, 299711, 3515, 8271, 16537, 107094, 81327, 11044, 299399, 71715, 154123, 32440, 16413, 169052, 42581, + 104608, 33812, 5696, 16661, 103419, 161, 39832, 179084, 236109, 71375, 67676, 75508, 93156, 21777, 80970, 58192, + 43293, 31757, 51423, 41531, 128929, 182898, 12880, 113231, 42107, 61632, 45914, 4884, 67180, 4744, 128700, 2781, + 25201, 36266, 194380, 87971, 115254, 341, 41014, 57871, 185488, 92043, 17835, 89050, 130954, 19517, 84683, 21380, + 72813, 45915, 93851, 203411, 167547, 176973, 63085, 59916, 20537, 17002, 36711, 31276, 39969, 36726, 65357, 13243, + 38432, 15644, 94063, 10719, 22582, 47135, 16038, 5381, 184022, 23165, 76012, 35198, 1139, 18638, 45545, 84452, + 27199, 192134, 119684, 123811, 5655, 13706, 141932, 24822, 17767, 37181, 5142, 34476, 97412, 225589, 175180, 68777, + 122606, 11285, 10611, 55686, 209377, 100096, 22340, 26689, 27070, 51760, 149649, 30372, 35871, 50512, 21058, 17439, + 326617, 170142, 107982, 135181, 188954, 85308, 56136, 9593, 42680, 26872, 58659, 5746, 73512, 25617, 2549, 48114, + 80911, 1733, 156604, 26196, 22629, 16115, 47515, 69763, 3011, 81888, 4772, 72580, 95021, 23422, 61841, 69210, + 315242, 20699, 13055, 19951, 157737, 52563, 31431, 59838, 383, 35462, 55449, 68880, 41821, 63984, 213573, 50441, + 41808, 53480, 40494, 130778, 19335, 64598, 138641, 25152, 27950, 8191, 57199, 35528, 15674, 204275, 70906, 3181, + 25677, 26876, 2717, 132658, 110950, 49839, 49173, 20862, 35375, 20135, 50308, 213100, 76835, 103314, 64615, 7399, + 59108, 22329, 92119, 34649, 57370, 20920, 11016, 129444, 35262, 68761, 92220, 17938, 16569, 14039, 59057, 72434, + 160415, 16248, 7148, 40010, 37706, 58080, 149680, 137070, 78086, 105307, 67671, 478, 32041, 27870, 179796, 13035, + 49691, 26716, 81195, 147295, 137143, 13139, 168200, 45495, 9782, 24335, 30927, 557, 172080, 226060, 57625, 14169, + 50148, 53124, 40398, 22321, 77917, 74830, 6334, 70846, 6323, 77024, 9517, 93307, 10110, 13831, 4136, 54992, + 69172, 15584, 33047, 77148, 17711, 31085, 33621, 126215, 21795, 114268, 35065, 145060, 59511, 11859, 154026, 131303, + 76184, 102024, 58089, 66420, 135114, 32471, 26586, 9983, 31046, 232116, 194394, 99288, 132319, 610, 10459, 98229, + 59105, 34807, 29993, 22965, 157578, 4107, 28141, 140655, 20549, 7101, 7846, 55412, 80778, 17135, 7430, 73220, + 57649, 27939, 10941, 92844, 158421, 173174, 64726, 12726, 65143, 202755, 176021, 57189, 4575, 7195, 177904, 25156, + 72235, 146111, 11686, 22007, 21899, 135284, 138978, 752, 10797, 65724, 5168, 151662, 92745, 109290, 75372, 160210, + 34035, 17369, 97529, 60335, 106079, 2306, 2423, 4131, 80159, 158934, 136359, 59711, 4508, 40343, 250673, 65860, + 78304, 17795, 104032, 148124, 25350, 58256, 33525, 20642, 75457, 81761, 183350, 24569, 46458, 63924, 58666, 8047, + 32937, 81997, 33987, 7245, 25623, 17931, 5112, 122123, 47, 80630, 79317, 15250, 8531, 7845, 42854, 87493, + 104751, 31479, 59823, 168974, 84953, 28434, 95840, 86398, 8138, 40995, 4860, 26024, 36508, 101200, 49636, 8174, + 187199, 50053, 89152, 20854, 66310, 61067, 8004, 30413, 115274, 278866, 106773, 120445, 13253, 40328, 1516, 70360, + 32461, 1703, 301530, 572, 38536, 75536, 423620, 18713, 1916, 3143, 70650, 60724, 42007, 14851, 262515, 136679, + 187160, 70985, 131034, 54573, 35055, 14435, 225137, 23005, 26325, 174156, 20786, 195824, 84394, 19162, 85376, 70194, + 35963, 49566, 21279, 91399, 94216, 64873, 68891, 55512, 45590, 3382, 26979, 72069, 97782, 126859, 187860, 246200 +] diff --git a/packages/kad-dht/src/routing-table/index.ts b/packages/kad-dht/src/routing-table/index.ts new file mode 100644 index 0000000000..a3f2afb83a --- /dev/null +++ b/packages/kad-dht/src/routing-table/index.ts @@ -0,0 +1,333 @@ +import { EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { PeerSet } from '@libp2p/peer-collections' +import Queue from 'p-queue' +import * as utils from '../utils.js' +import { KBucket, type PingEventDetails } from './k-bucket.js' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Metric, Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Logger } from '@libp2p/logger' + +export const KAD_CLOSE_TAG_NAME = 'kad-close' +export const KAD_CLOSE_TAG_VALUE = 50 +export const KBUCKET_SIZE = 20 +export const PING_TIMEOUT = 10000 +export const PING_CONCURRENCY = 10 + +export interface RoutingTableInit { + lan: boolean + protocol: string + kBucketSize?: number + pingTimeout?: number + pingConcurrency?: number + tagName?: string + tagValue?: number +} + +export interface RoutingTableComponents { + peerId: PeerId + peerStore: PeerStore + connectionManager: ConnectionManager + metrics?: Metrics +} + +export interface RoutingTableEvents { + 'peer:add': CustomEvent + 'peer:remove': CustomEvent +} + +/** + * A wrapper around `k-bucket`, to provide easy store and + * retrieval for peers. + */ +export class RoutingTable extends EventEmitter implements Startable { + public kBucketSize: number + public kb?: KBucket + public pingQueue: Queue + + private readonly log: Logger + private readonly components: RoutingTableComponents + private readonly lan: boolean + private readonly pingTimeout: number + private readonly pingConcurrency: number + private running: boolean + private readonly protocol: string + private readonly tagName: string + private readonly tagValue: number + private metrics?: { + routingTableSize: Metric + pingQueueSize: Metric + pingRunning: Metric + } + + constructor (components: RoutingTableComponents, init: RoutingTableInit) { + super() + + const { kBucketSize, pingTimeout, lan, pingConcurrency, protocol, tagName, tagValue } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:routing-table`) + this.kBucketSize = kBucketSize ?? KBUCKET_SIZE + this.pingTimeout = pingTimeout ?? PING_TIMEOUT + this.pingConcurrency = pingConcurrency ?? PING_CONCURRENCY + this.lan = lan + this.running = false + this.protocol = protocol + this.tagName = tagName ?? KAD_CLOSE_TAG_NAME + this.tagValue = tagValue ?? KAD_CLOSE_TAG_VALUE + + const updatePingQueueSizeMetric = (): void => { + this.metrics?.pingQueueSize.update(this.pingQueue.size) + this.metrics?.pingRunning.update(this.pingQueue.pending) + } + + this.pingQueue = new Queue({ concurrency: this.pingConcurrency }) + this.pingQueue.addListener('add', updatePingQueueSizeMetric) + this.pingQueue.addListener('next', updatePingQueueSizeMetric) + + this._onPing = this._onPing.bind(this) + } + + isStarted (): boolean { + return this.running + } + + async start (): Promise { + this.running = true + + if (this.components.metrics != null) { + this.metrics = { + routingTableSize: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_routing_table_size`), + pingQueueSize: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_ping_queue_size`), + pingRunning: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_ping_running`) + } + } + + const kBuck = new KBucket({ + localNodeId: await utils.convertPeerId(this.components.peerId), + numberOfNodesPerKBucket: this.kBucketSize, + numberOfNodesToPing: 1 + }) + this.kb = kBuck + + // test whether to evict peers + kBuck.addEventListener('ping', this._onPing) + + // tag kad-close peers + this._tagPeers(kBuck) + } + + async stop (): Promise { + this.running = false + this.pingQueue.clear() + this.kb = undefined + } + + /** + * Keep track of our k-closest peers and tag them in the peer store as such + * - this will lower the chances that connections to them get closed when + * we reach connection limits + */ + _tagPeers (kBuck: KBucket): void { + let kClosest = new PeerSet() + + const updatePeerTags = utils.debounce(() => { + const newClosest = new PeerSet( + kBuck.closest(kBuck.localNodeId, KBUCKET_SIZE).map(contact => contact.peer) + ) + const addedPeers = newClosest.difference(kClosest) + const removedPeers = kClosest.difference(newClosest) + + Promise.resolve() + .then(async () => { + for (const peer of addedPeers) { + await this.components.peerStore.merge(peer, { + tags: { + [this.tagName]: { + value: this.tagValue + } + } + }) + } + + for (const peer of removedPeers) { + await this.components.peerStore.merge(peer, { + tags: { + [this.tagName]: undefined + } + }) + } + }) + .catch(err => { + this.log.error('Could not update peer tags', err) + }) + + kClosest = newClosest + }) + + kBuck.addEventListener('added', (evt) => { + updatePeerTags() + + this.safeDispatchEvent('peer:add', { detail: evt.detail.peer }) + }) + kBuck.addEventListener('removed', (evt) => { + updatePeerTags() + + this.safeDispatchEvent('peer:remove', { detail: evt.detail.peer }) + }) + } + + /** + * Called on the `ping` event from `k-bucket` when a bucket is full + * and cannot split. + * + * `oldContacts.length` is defined by the `numberOfNodesToPing` param + * passed to the `k-bucket` constructor. + * + * `oldContacts` will not be empty and is the list of contacts that + * have not been contacted for the longest. + */ + _onPing (evt: CustomEvent): void { + const { + oldContacts, + newContact + } = evt.detail + + // add to a queue so multiple ping requests do not overlap and we don't + // flood the network with ping requests if lots of newContact requests + // are received + this.pingQueue.add(async () => { + if (!this.running) { + return + } + + let responded = 0 + + try { + await Promise.all( + oldContacts.map(async oldContact => { + try { + const options = { + signal: AbortSignal.timeout(this.pingTimeout) + } + + this.log('pinging old contact %p', oldContact.peer) + const connection = await this.components.connectionManager.openConnection(oldContact.peer, options) + const stream = await connection.newStream(this.protocol, options) + stream.close() + responded++ + } catch (err: any) { + if (this.running && this.kb != null) { + // only evict peers if we are still running, otherwise we evict when dialing is + // cancelled due to shutdown in progress + this.log.error('could not ping peer %p', oldContact.peer, err) + this.log('evicting old contact after ping failed %p', oldContact.peer) + this.kb.remove(oldContact.id) + } + } finally { + this.metrics?.routingTableSize.update(this.size) + } + }) + ) + + if (this.running && responded < oldContacts.length && this.kb != null) { + this.log('adding new contact %p', newContact.peer) + this.kb.add(newContact) + } + } catch (err: any) { + this.log.error('could not process k-bucket ping event', err) + } + }) + .catch(err => { + this.log.error('could not process k-bucket ping event', err) + }) + } + + // -- Public Interface + + /** + * Amount of currently stored peers + */ + get size (): number { + if (this.kb == null) { + return 0 + } + + return this.kb.count() + } + + /** + * Find a specific peer by id + */ + async find (peer: PeerId): Promise { + const key = await utils.convertPeerId(peer) + const closest = this.closestPeer(key) + + if (closest != null && peer.equals(closest)) { + return closest + } + + return undefined + } + + /** + * Retrieve the closest peers to the given key + */ + closestPeer (key: Uint8Array): PeerId | undefined { + const res = this.closestPeers(key, 1) + + if (res.length > 0) { + return res[0] + } + + return undefined + } + + /** + * Retrieve the `count`-closest peers to the given key + */ + closestPeers (key: Uint8Array, count = this.kBucketSize): PeerId[] { + if (this.kb == null) { + return [] + } + + const closest = this.kb.closest(key, count) + + return closest.map(p => p.peer) + } + + /** + * Add or update the routing table with the given peer + */ + async add (peer: PeerId): Promise { + if (this.kb == null) { + throw new Error('RoutingTable is not started') + } + + const id = await utils.convertPeerId(peer) + + this.kb.add({ id, peer }) + + this.log('added %p with kad id %b', peer, id) + + this.metrics?.routingTableSize.update(this.size) + } + + /** + * Remove a given peer from the table + */ + async remove (peer: PeerId): Promise { + if (this.kb == null) { + throw new Error('RoutingTable is not started') + } + + const id = await utils.convertPeerId(peer) + + this.kb.remove(id) + + this.metrics?.routingTableSize.update(this.size) + } +} diff --git a/packages/kad-dht/src/routing-table/k-bucket.ts b/packages/kad-dht/src/routing-table/k-bucket.ts new file mode 100644 index 0000000000..e49f1d0af2 --- /dev/null +++ b/packages/kad-dht/src/routing-table/k-bucket.ts @@ -0,0 +1,523 @@ +/* +index.js - Kademlia DHT K-bucket implementation as a binary tree. + +The MIT License (MIT) + +Copyright (c) 2013-2021 Tristan Slominski + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +import { EventEmitter } from '@libp2p/interfaces/events' +import type { PeerId } from '@libp2p/interface-peer-id' + +function arrayEquals (array1: Uint8Array, array2: Uint8Array): boolean { + if (array1 === array2) { + return true + } + if (array1.length !== array2.length) { + return false + } + for (let i = 0, length = array1.length; i < length; ++i) { + if (array1[i] !== array2[i]) { + return false + } + } + return true +} + +function createNode (): Bucket { + // @ts-expect-error loose types + return { contacts: [], dontSplit: false, left: null, right: null } +} + +function ensureInt8 (name: string, val?: Uint8Array): void { + if (!(val instanceof Uint8Array)) { + throw new TypeError(name + ' is not a Uint8Array') + } +} + +export interface PingEventDetails { + oldContacts: Contact[] + newContact: Contact +} + +export interface UpdatedEventDetails { + incumbent: Contact + selection: Contact +} + +export interface KBucketEvents { + 'ping': CustomEvent + 'added': CustomEvent + 'removed': CustomEvent + 'updated': CustomEvent +} + +export interface KBucketOptions { + /** + * A Uint8Array representing the local node id + */ + localNodeId: Uint8Array + + /** + * The number of nodes that a k-bucket can contain before being full or split. + */ + numberOfNodesPerKBucket?: number + + /** + * The number of nodes to ping when a bucket that should not be split becomes + * full. KBucket will emit a `ping` event that contains `numberOfNodesToPing` + * nodes that have not been contacted the longest. + */ + numberOfNodesToPing?: number + + /** + * An optional `distance` function that gets two `id` Uint8Arrays and return + * distance (as number) between them. + */ + 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 +} + +export interface Contact { + id: Uint8Array + peer: PeerId + vectorClock?: number +} + +export interface Bucket { + id: Uint8Array + contacts: Contact[] + dontSplit: boolean + left: Bucket + right: Bucket +} + +/** + * Implementation of a Kademlia DHT k-bucket used for storing + * contact (peer node) information. + * + * @extends EventEmitter + */ +export class KBucket extends EventEmitter { + public localNodeId: Uint8Array + public root: Bucket + private readonly numberOfNodesPerKBucket: number + private readonly numberOfNodesToPing: number + private readonly distance: (a: Uint8Array, b: Uint8Array) => number + private readonly arbiter: (incumbent: Contact, candidate: Contact) => Contact + + constructor (options: KBucketOptions) { + super() + + this.localNodeId = options.localNodeId + this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket ?? 20 + this.numberOfNodesToPing = options.numberOfNodesToPing ?? 3 + this.distance = options.distance ?? KBucket.distance + // use an arbiter from options or vectorClock arbiter by default + this.arbiter = options.arbiter ?? KBucket.arbiter + + ensureInt8('option.localNodeId as parameter 1', this.localNodeId) + + this.root = createNode() + } + + /** + * Default arbiter function for contacts with the same id. Uses + * contact.vectorClock to select which contact to update the k-bucket with. + * Contact with larger vectorClock field will be selected. If vectorClock is + * the same, candidate will be selected. + * + * @param {object} incumbent - Contact currently stored in the k-bucket. + * @param {object} candidate - Contact being added to the k-bucket. + * @returns {object} Contact to updated the k-bucket with. + */ + static arbiter (incumbent: Contact, candidate: Contact): Contact { + return (incumbent.vectorClock ?? 0) > (candidate.vectorClock ?? 0) ? incumbent : candidate + } + + /** + * Default distance function. Finds the XOR + * distance between firstId and secondId. + * + * @param {Uint8Array} firstId - Uint8Array containing first id. + * @param {Uint8Array} secondId - Uint8Array containing second id. + * @returns {number} Integer The XOR distance between firstId and secondId. + */ + static distance (firstId: Uint8Array, secondId: Uint8Array): number { + let distance = 0 + let i = 0 + const min = Math.min(firstId.length, secondId.length) + const max = Math.max(firstId.length, secondId.length) + for (; i < min; ++i) { + distance = distance * 256 + (firstId[i] ^ secondId[i]) + } + for (; i < max; ++i) distance = distance * 256 + 255 + return distance + } + + /** + * Adds a contact to the k-bucket. + * + * @param {object} contact - the contact object to add + */ + add (contact: Contact): KBucket { + ensureInt8('contact.id', contact?.id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + // this is not a leaf node but an inner node with 'low' and 'high' + // branches; we will check the appropriate bit of the identifier and + // delegate to the appropriate node for further processing + node = this._determineNode(node, contact.id, bitIndex++) + } + + // check if the contact already exists + const index = this._indexOf(node, contact.id) + if (index >= 0) { + this._update(node, index, contact) + return this + } + + if (node.contacts.length < this.numberOfNodesPerKBucket) { + node.contacts.push(contact) + this.safeDispatchEvent('added', { detail: contact }) + return this + } + + // the bucket is full + if (node.dontSplit) { + // we are not allowed to split the bucket + // we need to ping the first this.numberOfNodesToPing + // in order to determine if they are alive + // only if one of the pinged nodes does not respond, can the new contact + // be added (this prevents DoS flodding with new invalid contacts) + this.safeDispatchEvent('ping', { + detail: { + oldContacts: node.contacts.slice(0, this.numberOfNodesToPing), + newContact: contact + } + }) + return this + } + + this._split(node, bitIndex) + return this.add(contact) + } + + /** + * Get the n closest contacts to the provided node id. "Closest" here means: + * closest according to the XOR metric of the contact node id. + * + * @param {Uint8Array} id - Contact node id + * @param {number} n - Integer (Default: Infinity) The maximum number of closest contacts to return + * @returns {Array} Array Maximum of n closest contacts to the node id + */ + closest (id: Uint8Array, n = Infinity): Contact[] { + ensureInt8('id', id) + + if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) { + throw new TypeError('n is not positive number') + } + + let contacts: Contact[] = [] + + for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + const detNode = this._determineNode(node, id, bitIndex++) + nodes.push(node.left === detNode ? node.right : node.left) + nodes.push(detNode) + } else { + contacts = contacts.concat(node.contacts) + } + } + + return contacts + .map(a => ({ + distance: this.distance(a.id, id), + contact: a + })) + .sort((a, b) => a.distance - b.distance) + .slice(0, n) + .map(a => a.contact) + } + + /** + * Counts the total number of contacts in the tree. + * + * @returns {number} The number of contacts held in the tree + */ + count (): number { + // return this.toArray().length + let count = 0 + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + nodes.push(node.right, node.left) + } else { + count += node.contacts.length + } + } + + return count + } + + /** + * Determines whether the id at the bitIndex is 0 or 1. + * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise + * + * @param {object} node - internal object that has 2 leafs: left and right + * @param {Uint8Array} id - Id to compare localNodeId with. + * @param {number} bitIndex - Integer (Default: 0) The bit index to which bit to check in the id Uint8Array. + * @returns {object} left leaf if id at bitIndex is 0, right leaf otherwise. + */ + _determineNode (node: any, id: Uint8Array, bitIndex: number): Bucket { + // **NOTE** remember that id is a Uint8Array and has granularity of + // bytes (8 bits), whereas the bitIndex is the _bit_ index (not byte) + + // id's that are too short are put in low bucket (1 byte = 8 bits) + // (bitIndex >> 3) finds how many bytes the bitIndex describes + // bitIndex % 8 checks if we have extra bits beyond byte multiples + // if number of bytes is <= no. of bytes described by bitIndex and there + // are extra bits to consider, this means id has less bits than what + // bitIndex describes, id therefore is too short, and will be put in low + // bucket + const bytesDescribedByBitIndex = bitIndex >> 3 + const bitIndexWithinByte = bitIndex % 8 + if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) { + return node.left + } + + const byteUnderConsideration = id[bytesDescribedByBitIndex] + + // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits + // where 255 is 11111111 and 0 is 00000000 + // in order to find out whether the bit at bitIndexWithinByte is set + // we construct (1 << (7 - bitIndexWithinByte)) which will consist + // of all bits being 0, with only one bit set to 1 + // for example, if bitIndexWithinByte is 3, we will construct 00010000 by + // (1 << (7 - 3)) -> (1 << 4) -> 16 + if ((byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) !== 0) { + return node.right + } + + return node.left + } + + /** + * Get a contact by its exact ID. + * If this is a leaf, loop through the bucket contents and return the correct + * contact if we have it or null if not. If this is an inner node, determine + * which branch of the tree to traverse and repeat. + * + * @param {Uint8Array} id - The ID of the contact to fetch. + * @returns {object | null} The contact if available, otherwise null + */ + get (id: Uint8Array): Contact | undefined { + ensureInt8('id', id) + + let bitIndex = 0 + + let node: Bucket = this.root + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + // index of uses contact id for matching + const index = this._indexOf(node, id) + return index >= 0 ? node.contacts[index] : undefined + } + + /** + * Returns the index of the contact with provided + * id if it exists, returns -1 otherwise. + * + * @param {object} node - internal object that has 2 leafs: left and right + * @param {Uint8Array} id - Contact node id. + * @returns {number} Integer Index of contact with provided id if it exists, -1 otherwise. + */ + _indexOf (node: Bucket, id: Uint8Array): number { + for (let i = 0; i < node.contacts.length; ++i) { + if (arrayEquals(node.contacts[i].id, id)) return i + } + + return -1 + } + + /** + * Removes contact with the provided id. + * + * @param {Uint8Array} id - The ID of the contact to remove + * @returns {object} The k-bucket itself + */ + remove (id: Uint8Array): KBucket { + ensureInt8('the id as parameter 1', id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + const index = this._indexOf(node, id) + if (index >= 0) { + const contact = node.contacts.splice(index, 1)[0] + this.safeDispatchEvent('removed', { + detail: contact + }) + } + + return this + } + + /** + * Splits the node, redistributes contacts to the new nodes, and marks the + * node that was split as an inner node of the binary tree of nodes by + * setting this.root.contacts = null + * + * @param {object} node - node for splitting + * @param {number} bitIndex - the bitIndex to which byte to check in the Uint8Array for navigating the binary tree + */ + _split (node: Bucket, bitIndex: number): void { + node.left = createNode() + node.right = createNode() + + // redistribute existing contacts amongst the two newly created nodes + for (const contact of node.contacts) { + this._determineNode(node, contact.id, bitIndex).contacts.push(contact) + } + + // @ts-expect-error loose types + node.contacts = null // mark as inner tree node + + // don't split the "far away" node + // we check where the local node would end up and mark the other one as + // "dontSplit" (i.e. "far away") + const detNode = this._determineNode(node, this.localNodeId, bitIndex) + const otherNode = node.left === detNode ? node.right : node.left + otherNode.dontSplit = true + } + + /** + * Returns all the contacts contained in the tree as an array. + * If this is a leaf, return a copy of the bucket. If this is not a leaf, + * return the union of the low and high branches (themselves also as arrays). + * + * @returns {Array} All of the contacts in the tree, as an array + */ + toArray (): Contact[] { + let result: Contact[] = [] + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + nodes.push(node.right, node.left) + } else { + result = result.concat(node.contacts) + } + } + return result + } + + /** + * Similar to `toArray()` but instead of buffering everything up into an + * array before returning it, yields contacts as they are encountered while + * walking the tree. + * + * @returns {Iterable} All of the contacts in the tree, as an iterable + */ + * toIterable (): Iterable { + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + nodes.push(node.right, node.left) + } else { + yield * node.contacts + } + } + } + + /** + * Updates the contact selected by the arbiter. + * If the selection is our old contact and the candidate is some new contact + * then the new contact is abandoned (not added). + * If the selection is our old contact and the candidate is our old contact + * then we are refreshing the contact and it is marked as most recently + * contacted (by being moved to the right/end of the bucket array). + * If the selection is our new contact, the old contact is removed and the new + * contact is marked as most recently contacted. + * + * @param {object} node - internal object that has 2 leafs: left and right + * @param {number} index - the index in the bucket where contact exists (index has already been computed in a previous calculation) + * @param {object} contact - The contact object to update + */ + _update (node: Bucket, index: number, contact: Contact): void { + // sanity check + if (!arrayEquals(node.contacts[index].id, contact.id)) { + throw new Error('wrong index for _update') + } + + const incumbent = node.contacts[index] + const selection = this.arbiter(incumbent, contact) + // if the selection is our old contact and the candidate is some new + // contact, then there is nothing to do + if (selection === incumbent && incumbent !== contact) return + + node.contacts.splice(index, 1) // remove old contact + node.contacts.push(selection) // add more recent contact version + this.safeDispatchEvent('updated', { + detail: { + incumbent, selection + } + }) + } +} diff --git a/packages/kad-dht/src/routing-table/refresh.ts b/packages/kad-dht/src/routing-table/refresh.ts new file mode 100644 index 0000000000..5c3d198c1c --- /dev/null +++ b/packages/kad-dht/src/routing-table/refresh.ts @@ -0,0 +1,257 @@ +import { randomBytes } from '@libp2p/crypto' +import { logger } from '@libp2p/logger' +import { peerIdFromBytes } from '@libp2p/peer-id' +import length from 'it-length' +import { sha256 } from 'multiformats/hashes/sha2' +import { xor as uint8ArrayXor } from 'uint8arrays/xor' +import { TABLE_REFRESH_INTERVAL, TABLE_REFRESH_QUERY_TIMEOUT } from '../constants.js' +import GENERATED_PREFIXES from './generated-prefix-list.js' +import type { RoutingTable } from './index.js' +import type { PeerRouting } from '../peer-routing/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Logger } from '@libp2p/logger' + +/** + * Cannot generate random KadIds longer than this + 1 + */ +const MAX_COMMON_PREFIX_LENGTH = 15 + +export interface RoutingTableRefreshInit { + peerRouting: PeerRouting + routingTable: RoutingTable + lan: boolean + refreshInterval?: number + refreshQueryTimeout?: number +} + +/** + * A wrapper around `k-bucket`, to provide easy store and + * retrieval for peers. + */ +export class RoutingTableRefresh { + private readonly log: Logger + private readonly peerRouting: PeerRouting + private readonly routingTable: RoutingTable + private readonly refreshInterval: number + private readonly refreshQueryTimeout: number + private readonly commonPrefixLengthRefreshedAt: Date[] + private refreshTimeoutId?: NodeJS.Timer + + constructor (init: RoutingTableRefreshInit) { + const { peerRouting, routingTable, refreshInterval, refreshQueryTimeout, lan } = init + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:routing-table:refresh`) + this.peerRouting = peerRouting + this.routingTable = routingTable + this.refreshInterval = refreshInterval ?? TABLE_REFRESH_INTERVAL + this.refreshQueryTimeout = refreshQueryTimeout ?? TABLE_REFRESH_QUERY_TIMEOUT + this.commonPrefixLengthRefreshedAt = [] + + this.refreshTable = this.refreshTable.bind(this) + } + + async start (): Promise { + this.log(`refreshing routing table every ${this.refreshInterval}ms`) + this.refreshTable(true) + } + + async stop (): Promise { + if (this.refreshTimeoutId != null) { + clearTimeout(this.refreshTimeoutId) + } + } + + /** + * To speed lookups, we seed the table with random PeerIds. This means + * when we are asked to locate a peer on the network, we can find a KadId + * that is close to the requested peer ID and query that, then network + * peers will tell us who they know who is close to the fake ID + */ + refreshTable (force: boolean = false): void { + this.log('refreshing routing table') + + const prefixLength = this._maxCommonPrefix() + const refreshCpls = this._getTrackedCommonPrefixLengthsForRefresh(prefixLength) + + this.log(`max common prefix length ${prefixLength}`) + this.log(`tracked CPLs [ ${refreshCpls.map(date => date.toISOString()).join(', ')} ]`) + + /** + * If we see a gap at a common prefix length in the Routing table, we ONLY refresh up until + * the maximum cpl we have in the Routing Table OR (2 * (Cpl+ 1) with the gap), whichever + * is smaller. + * + * This is to prevent refreshes for Cpls that have no peers in the network but happen to be + * before a very high max Cpl for which we do have peers in the network. + * + * The number of 2 * (Cpl + 1) can be proved and a proof would have been written here if + * the programmer had paid more attention in the Math classes at university. + * + * So, please be patient and a doc explaining it will be published soon. + * + * https://github.com/libp2p/go-libp2p-kad-dht/commit/2851c88acb0a3f86bcfe3cfd0f4604a03db801d8#diff-ad45f4ba97ffbc4083c2eb87a4420c1157057b233f048030d67c6b551855ccf6R219 + */ + Promise.all( + refreshCpls.map(async (lastRefresh, index) => { + try { + await this._refreshCommonPrefixLength(index, lastRefresh, force) + + if (this._numPeersForCpl(prefixLength) === 0) { + const lastCpl = Math.min(2 * (index + 1), refreshCpls.length - 1) + + for (let n = index + 1; n < lastCpl + 1; n++) { + try { + await this._refreshCommonPrefixLength(n, lastRefresh, force) + } catch (err: any) { + this.log.error(err) + } + } + } + } catch (err: any) { + this.log.error(err) + } + }) + ).catch(err => { + this.log.error(err) + }).then(() => { + this.refreshTimeoutId = setTimeout(this.refreshTable, this.refreshInterval) + + if (this.refreshTimeoutId.unref != null) { + this.refreshTimeoutId.unref() + } + }).catch(err => { + this.log.error(err) + }) + } + + async _refreshCommonPrefixLength (cpl: number, lastRefresh: Date, force: boolean): Promise { + if (!force && lastRefresh.getTime() > (Date.now() - this.refreshInterval)) { + this.log('not running refresh for cpl %s as time since last refresh not above interval', cpl) + return + } + + // gen a key for the query to refresh the cpl + const peerId = await this._generateRandomPeerId(cpl) + + this.log('starting refreshing cpl %s with key %p (routing table size was %s)', cpl, peerId, this.routingTable.size) + + const peers = await length(this.peerRouting.getClosestPeers(peerId.toBytes(), { signal: AbortSignal.timeout(this.refreshQueryTimeout) })) + + this.log(`found ${peers} peers that were close to imaginary peer %p`, peerId) + this.log('finished refreshing cpl %s with key %p (routing table size is now %s)', cpl, peerId, this.routingTable.size) + } + + _getTrackedCommonPrefixLengthsForRefresh (maxCommonPrefix: number): Date[] { + if (maxCommonPrefix > MAX_COMMON_PREFIX_LENGTH) { + maxCommonPrefix = MAX_COMMON_PREFIX_LENGTH + } + + const dates = [] + + for (let i = 0; i <= maxCommonPrefix; i++) { + // defaults to the zero value if we haven't refreshed it yet. + dates[i] = this.commonPrefixLengthRefreshedAt[i] ?? new Date() + } + + return dates + } + + async _generateRandomPeerId (targetCommonPrefixLength: number): Promise { + if (this.routingTable.kb == null) { + throw new Error('Routing table not started') + } + + const randomData = randomBytes(2) + const randomUint16 = (randomData[1] << 8) + randomData[0] + + const key = await this._makePeerId(this.routingTable.kb.localNodeId, randomUint16, targetCommonPrefixLength) + + return peerIdFromBytes(key) + } + + async _makePeerId (localKadId: Uint8Array, randomPrefix: number, targetCommonPrefixLength: number): Promise { + if (targetCommonPrefixLength > MAX_COMMON_PREFIX_LENGTH) { + throw new Error(`Cannot generate peer ID for common prefix length greater than ${MAX_COMMON_PREFIX_LENGTH}`) + } + + const view = new DataView(localKadId.buffer, localKadId.byteOffset, localKadId.byteLength) + const localPrefix = view.getUint16(0, false) + + // For host with ID `L`, an ID `K` belongs to a bucket with ID `B` ONLY IF CommonPrefixLen(L,K) is EXACTLY B. + // Hence, to achieve a targetPrefix `T`, we must toggle the (T+1)th bit in L & then copy (T+1) bits from L + // to our randomly generated prefix. + const toggledLocalPrefix = localPrefix ^ (0x8000 >> targetCommonPrefixLength) + + // Combine the toggled local prefix and the random bits at the correct offset + // such that ONLY the first `targetCommonPrefixLength` bits match the local ID. + const mask = 65535 << (16 - (targetCommonPrefixLength + 1)) + const targetPrefix = (toggledLocalPrefix & mask) | (randomPrefix & ~mask) + + // Convert to a known peer ID. + const keyPrefix = GENERATED_PREFIXES[targetPrefix] + + const keyBuffer = new ArrayBuffer(34) + const keyView = new DataView(keyBuffer, 0, keyBuffer.byteLength) + keyView.setUint8(0, sha256.code) + keyView.setUint8(1, 32) + keyView.setUint32(2, keyPrefix, false) + + return new Uint8Array(keyView.buffer, keyView.byteOffset, keyView.byteLength) + } + + /** + * returns the maximum common prefix length between any peer in the table + * and the current peer + */ + _maxCommonPrefix (): number { + // xor our KadId with every KadId in the k-bucket tree, + // return the longest id prefix that is the same + let prefixLength = 0 + + for (const length of this._prefixLengths()) { + if (length > prefixLength) { + prefixLength = length + } + } + + return prefixLength + } + + /** + * Returns the number of peers in the table with a given prefix length + */ + _numPeersForCpl (prefixLength: number): number { + let count = 0 + + for (const length of this._prefixLengths()) { + if (length === prefixLength) { + count++ + } + } + + return count + } + + /** + * Yields the common prefix length of every peer in the table + */ + * _prefixLengths (): Generator { + if (this.routingTable.kb == null) { + return + } + + for (const { id } of this.routingTable.kb.toIterable()) { + const distance = uint8ArrayXor(this.routingTable.kb.localNodeId, id) + let leadingZeros = 0 + + for (const byte of distance) { + if (byte === 0) { + leadingZeros++ + } else { + break + } + } + + yield leadingZeros + } + } +} diff --git a/packages/kad-dht/src/rpc/handlers/add-provider.ts b/packages/kad-dht/src/rpc/handlers/add-provider.ts new file mode 100644 index 0000000000..f01c5f21da --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/add-provider.ts @@ -0,0 +1,63 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { CID } from 'multiformats/cid' +import type { Message } from '../../message/index.js' +import type { Providers } from '../../providers' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const log = logger('libp2p:kad-dht:rpc:handlers:add-provider') + +export interface AddProviderHandlerInit { + providers: Providers +} + +export class AddProviderHandler implements DHTMessageHandler { + private readonly providers: Providers + + constructor (init: AddProviderHandlerInit) { + const { providers } = init + this.providers = providers + } + + async handle (peerId: PeerId, msg: Message): Promise { + log('start') + + if (msg.key == null || msg.key.length === 0) { + throw new CodeError('Missing key', 'ERR_MISSING_KEY') + } + + let cid: CID + try { + // this is actually just the multihash, not the whole CID + cid = CID.decode(msg.key) + } catch (err: any) { + throw new CodeError('Invalid CID', 'ERR_INVALID_CID') + } + + if (msg.providerPeers == null || msg.providerPeers.length === 0) { + log.error('no providers found in message') + } + + await Promise.all( + msg.providerPeers.map(async (pi) => { + // Ignore providers not from the originator + if (!pi.id.equals(peerId)) { + log('invalid provider peer %p from %p', pi.id, peerId) + return + } + + if (pi.multiaddrs.length < 1) { + log('no valid addresses for provider %p. Ignore', peerId) + return + } + + log('received provider %p for %s (addrs %s)', peerId, cid, pi.multiaddrs.map((m) => m.toString())) + + await this.providers.addProvider(cid, pi.id) + }) + ) + + return undefined + } +} diff --git a/packages/kad-dht/src/rpc/handlers/find-node.ts b/packages/kad-dht/src/rpc/handlers/find-node.ts new file mode 100644 index 0000000000..1bf7b6ce47 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/find-node.ts @@ -0,0 +1,72 @@ +import { logger } from '@libp2p/logger' +import { protocols } from '@multiformats/multiaddr' +import { equals as uint8ArrayEquals } from 'uint8arrays' +import { Message } from '../../message/index.js' +import { + removePrivateAddresses, + removePublicAddresses +} from '../../utils.js' +import type { PeerRouting } from '../../peer-routing/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' + +const log = logger('libp2p:kad-dht:rpc:handlers:find-node') + +export interface FindNodeHandlerInit { + peerRouting: PeerRouting + lan: boolean +} + +export interface FindNodeHandlerComponents { + peerId: PeerId + addressManager: AddressManager +} + +export class FindNodeHandler implements DHTMessageHandler { + private readonly peerRouting: PeerRouting + private readonly lan: boolean + private readonly components: FindNodeHandlerComponents + + constructor (components: FindNodeHandlerComponents, init: FindNodeHandlerInit) { + const { peerRouting, lan } = init + + this.components = components + this.peerRouting = peerRouting + this.lan = Boolean(lan) + } + + /** + * Process `FindNode` DHT messages + */ + async handle (peerId: PeerId, msg: Message): Promise { + log('incoming request from %p for peers closer to %b', peerId, msg.key) + + let closer: PeerInfo[] = [] + + if (uint8ArrayEquals(this.components.peerId.toBytes(), msg.key)) { + closer = [{ + id: this.components.peerId, + multiaddrs: this.components.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code)), + protocols: [] + }] + } else { + closer = await this.peerRouting.getCloserPeersOffline(msg.key, peerId) + } + + closer = closer + .map(this.lan ? removePublicAddresses : removePrivateAddresses) + .filter(({ multiaddrs }) => multiaddrs.length) + + const response = new Message(msg.type, new Uint8Array(0), msg.clusterLevel) + + if (closer.length > 0) { + response.closerPeers = closer + } else { + log('could not find any peers closer to %b than %p', msg.key, peerId) + } + + return response + } +} diff --git a/packages/kad-dht/src/rpc/handlers/get-providers.ts b/packages/kad-dht/src/rpc/handlers/get-providers.ts new file mode 100644 index 0000000000..c213591b61 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/get-providers.ts @@ -0,0 +1,105 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { CID } from 'multiformats/cid' +import { Message } from '../../message/index.js' +import { + removePrivateAddresses, + removePublicAddresses +} from '../../utils.js' +import type { PeerRouting } from '../../peer-routing/index.js' +import type { Providers } from '../../providers.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:kad-dht:rpc:handlers:get-providers') + +export interface GetProvidersHandlerInit { + peerRouting: PeerRouting + providers: Providers + lan: boolean +} + +export interface GetProvidersHandlerComponents { + peerStore: PeerStore +} + +export class GetProvidersHandler implements DHTMessageHandler { + private readonly components: GetProvidersHandlerComponents + private readonly peerRouting: PeerRouting + private readonly providers: Providers + private readonly lan: boolean + + constructor (components: GetProvidersHandlerComponents, init: GetProvidersHandlerInit) { + const { peerRouting, providers, lan } = init + + this.components = components + this.peerRouting = peerRouting + this.providers = providers + this.lan = Boolean(lan) + } + + async handle (peerId: PeerId, msg: Message): Promise { + let cid + try { + cid = CID.decode(msg.key) + } catch (err: any) { + throw new CodeError('Invalid CID', 'ERR_INVALID_CID') + } + + log('%p asking for providers for %s', peerId, cid) + + const [peers, closer] = await Promise.all([ + this.providers.getProviders(cid), + this.peerRouting.getCloserPeersOffline(msg.key, peerId) + ]) + + const providerPeers = await this._getPeers(peers) + const closerPeers = await this._getPeers(closer.map(({ id }) => id)) + const response = new Message(msg.type, msg.key, msg.clusterLevel) + + if (providerPeers.length > 0) { + response.providerPeers = providerPeers + } + + if (closerPeers.length > 0) { + response.closerPeers = closerPeers + } + + log('got %s providers %s closerPeers', providerPeers.length, closerPeers.length) + return response + } + + async _getAddresses (peerId: PeerId): Promise { + return [] + } + + async _getPeers (peerIds: PeerId[]): Promise { + const output: PeerInfo[] = [] + const addrFilter = this.lan ? removePublicAddresses : removePrivateAddresses + + for (const peerId of peerIds) { + try { + const peer = await this.components.peerStore.get(peerId) + + const peerAfterFilter = addrFilter({ + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + }) + + if (peerAfterFilter.multiaddrs.length > 0) { + output.push(peerAfterFilter) + } + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + return output + } +} diff --git a/packages/kad-dht/src/rpc/handlers/get-value.ts b/packages/kad-dht/src/rpc/handlers/get-value.ts new file mode 100644 index 0000000000..d5a7c0a311 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/get-value.ts @@ -0,0 +1,131 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { Libp2pRecord } from '@libp2p/record' +import { + MAX_RECORD_AGE +} from '../../constants.js' +import { Message, MESSAGE_TYPE } from '../../message/index.js' +import { bufferToRecordKey, isPublicKeyKey, fromPublicKeyKey } from '../../utils.js' +import type { PeerRouting } from '../../peer-routing/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Datastore } from 'interface-datastore' + +const log = logger('libp2p:kad-dht:rpc:handlers:get-value') + +export interface GetValueHandlerInit { + peerRouting: PeerRouting +} + +export interface GetValueHandlerComponents { + peerStore: PeerStore + datastore: Datastore +} + +export class GetValueHandler implements DHTMessageHandler { + private readonly components: GetValueHandlerComponents + private readonly peerRouting: PeerRouting + + constructor (components: GetValueHandlerComponents, init: GetValueHandlerInit) { + const { peerRouting } = init + + this.components = components + this.peerRouting = peerRouting + } + + async handle (peerId: PeerId, msg: Message): Promise { + const key = msg.key + + log('%p asked for key %b', peerId, key) + + if (key == null || key.length === 0) { + throw new CodeError('Invalid key', 'ERR_INVALID_KEY') + } + + const response = new Message(MESSAGE_TYPE.GET_VALUE, key, msg.clusterLevel) + + if (isPublicKeyKey(key)) { + log('is public key') + const idFromKey = fromPublicKeyKey(key) + let pubKey: Uint8Array | undefined + + try { + const peer = await this.components.peerStore.get(idFromKey) + + if (peer.id.publicKey == null) { + throw new CodeError('No public key found in key book', 'ERR_NOT_FOUND') + } + + pubKey = peer.id.publicKey + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + + if (pubKey != null) { + log('returning found public key') + response.record = new Libp2pRecord(key, pubKey, new Date()) + return response + } + } + + const [record, closer] = await Promise.all([ + this._checkLocalDatastore(key), + this.peerRouting.getCloserPeersOffline(msg.key, peerId) + ]) + + if (record != null) { + log('had record for %b in local datastore', key) + response.record = record + } + + if (closer.length > 0) { + log('had %s closer peers in routing table', closer.length) + response.closerPeers = closer + } + + return response + } + + /** + * Try to fetch a given record by from the local datastore. + * Returns the record if it is still valid, meaning + * - it was either authored by this node, or + * - it was received less than `MAX_RECORD_AGE` ago. + */ + async _checkLocalDatastore (key: Uint8Array): Promise { + log('checkLocalDatastore looking for %b', key) + const dsKey = bufferToRecordKey(key) + + // Fetch value from ds + let rawRecord + try { + rawRecord = await this.components.datastore.get(dsKey) + } catch (err: any) { + if (err.code === 'ERR_NOT_FOUND') { + return undefined + } + throw err + } + + // Create record from the returned bytes + const record = Libp2pRecord.deserialize(rawRecord) + + if (record == null) { + throw new CodeError('Invalid record', 'ERR_INVALID_RECORD') + } + + // Check validity: compare time received with max record age + if (record.timeReceived == null || + Date.now() - record.timeReceived.getTime() > MAX_RECORD_AGE) { + // If record is bad delete it and return + await this.components.datastore.delete(dsKey) + return undefined + } + + // Record is valid + return record + } +} diff --git a/packages/kad-dht/src/rpc/handlers/ping.ts b/packages/kad-dht/src/rpc/handlers/ping.ts new file mode 100644 index 0000000000..09d569c06e --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/ping.ts @@ -0,0 +1,13 @@ +import { logger } from '@libp2p/logger' +import type { Message } from '../../message/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const log = logger('libp2p:kad-dht:rpc:handlers:ping') + +export class PingHandler implements DHTMessageHandler { + async handle (peerId: PeerId, msg: Message): Promise { + log('ping from %p', peerId) + return msg + } +} diff --git a/packages/kad-dht/src/rpc/handlers/put-value.ts b/packages/kad-dht/src/rpc/handlers/put-value.ts new file mode 100644 index 0000000000..20a4084948 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/put-value.ts @@ -0,0 +1,58 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { type Logger, logger } from '@libp2p/logger' +import { verifyRecord } from '@libp2p/record/validators' +import { bufferToRecordKey } from '../../utils.js' +import type { Validators } from '../../index.js' +import type { Message } from '../../message/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +export interface PutValueHandlerInit { + validators: Validators +} + +export interface PutValueHandlerComponents { + datastore: Datastore +} + +export class PutValueHandler implements DHTMessageHandler { + private readonly log: Logger + private readonly components: PutValueHandlerComponents + private readonly validators: Validators + + constructor (components: PutValueHandlerComponents, init: PutValueHandlerInit) { + const { validators } = init + + this.components = components + this.log = logger('libp2p:kad-dht:rpc:handlers:put-value') + this.validators = validators + } + + async handle (peerId: PeerId, msg: Message): Promise { + const key = msg.key + this.log('%p asked us to store value for key %b', peerId, key) + + const record = msg.record + + if (record == null) { + const errMsg = `Empty record from: ${peerId.toString()}` + + this.log.error(errMsg) + throw new CodeError(errMsg, 'ERR_EMPTY_RECORD') + } + + try { + await verifyRecord(this.validators, record) + + record.timeReceived = new Date() + const recordKey = bufferToRecordKey(record.key) + await this.components.datastore.put(recordKey, record.serialize().subarray()) + this.log('put record for %b into datastore under key %k', key, recordKey) + } catch (err: any) { + this.log('did not put record for key %b into datastore %o', key, err) + } + + return msg + } +} diff --git a/packages/kad-dht/src/rpc/index.ts b/packages/kad-dht/src/rpc/index.ts new file mode 100644 index 0000000000..fc435fe062 --- /dev/null +++ b/packages/kad-dht/src/rpc/index.ts @@ -0,0 +1,115 @@ +import { type Logger, logger } from '@libp2p/logger' +import * as lp from 'it-length-prefixed' +import { pipe } from 'it-pipe' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { AddProviderHandler } from './handlers/add-provider.js' +import { FindNodeHandler, type FindNodeHandlerComponents } from './handlers/find-node.js' +import { GetProvidersHandler, type GetProvidersHandlerComponents } from './handlers/get-providers.js' +import { GetValueHandler, type GetValueHandlerComponents } from './handlers/get-value.js' +import { PingHandler } from './handlers/ping.js' +import { PutValueHandler, type PutValueHandlerComponents } from './handlers/put-value.js' +import type { Validators } from '../index.js' +import type { PeerRouting } from '../peer-routing' +import type { Providers } from '../providers' +import type { RoutingTable } from '../routing-table' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { IncomingStreamData } from '@libp2p/interface-registrar' + +export interface DHTMessageHandler { + handle: (peerId: PeerId, msg: Message) => Promise +} + +export interface RPCInit { + routingTable: RoutingTable + providers: Providers + peerRouting: PeerRouting + validators: Validators + lan: boolean +} + +export interface RPCComponents extends GetValueHandlerComponents, PutValueHandlerComponents, FindNodeHandlerComponents, GetProvidersHandlerComponents { + +} + +export class RPC { + private readonly handlers: Record + private readonly routingTable: RoutingTable + private readonly log: Logger + + constructor (components: RPCComponents, init: RPCInit) { + const { providers, peerRouting, validators, lan } = init + + this.log = logger('libp2p:kad-dht:rpc') + this.routingTable = init.routingTable + this.handlers = { + [MESSAGE_TYPE.GET_VALUE]: new GetValueHandler(components, { peerRouting }), + [MESSAGE_TYPE.PUT_VALUE]: new PutValueHandler(components, { validators }), + [MESSAGE_TYPE.FIND_NODE]: new FindNodeHandler(components, { peerRouting, lan }), + [MESSAGE_TYPE.ADD_PROVIDER]: new AddProviderHandler({ providers }), + [MESSAGE_TYPE.GET_PROVIDERS]: new GetProvidersHandler(components, { peerRouting, providers, lan }), + [MESSAGE_TYPE.PING]: new PingHandler() + } + } + + /** + * Process incoming DHT messages + */ + async handleMessage (peerId: PeerId, msg: Message): Promise { + try { + await this.routingTable.add(peerId) + } catch (err: any) { + this.log.error('Failed to update the kbucket store', err) + } + + // get handler & execute it + const handler = this.handlers[msg.type] + + if (handler == null) { + this.log.error(`no handler found for message type: ${msg.type}`) + return + } + + return handler.handle(peerId, msg) + } + + /** + * Handle incoming streams on the dht protocol + */ + onIncomingStream (data: IncomingStreamData): void { + Promise.resolve().then(async () => { + const { stream, connection } = data + const peerId = connection.remotePeer + + try { + await this.routingTable.add(peerId) + } catch (err: any) { + this.log.error(err) + } + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + await pipe( + stream, + (source) => lp.decode(source), + async function * (source) { + for await (const msg of source) { + // handle the message + const desMessage = Message.deserialize(msg) + self.log('incoming %s from %p', desMessage.type, peerId) + const res = await self.handleMessage(peerId, desMessage) + + // Not all handlers will return a response + if (res != null) { + yield res.serialize() + } + } + }, + (source) => lp.encode(source), + stream + ) + }) + .catch(err => { + this.log.error(err) + }) + } +} diff --git a/packages/kad-dht/src/topology-listener.ts b/packages/kad-dht/src/topology-listener.ts new file mode 100644 index 0000000000..162e2c782c --- /dev/null +++ b/packages/kad-dht/src/topology-listener.ts @@ -0,0 +1,77 @@ +import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { createTopology } from '@libp2p/topology' +import type { KadDHTComponents } from '.' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Logger } from '@libp2p/logger' + +export interface TopologyListenerInit { + protocol: string + lan: boolean +} + +export interface TopologyListenerEvents { + 'peer': CustomEvent +} + +/** + * Receives notifications of new peers joining the network that support the DHT protocol + */ +export class TopologyListener extends EventEmitter implements Startable { + private readonly log: Logger + private readonly components: KadDHTComponents + private readonly protocol: string + private running: boolean + private registrarId?: string + + constructor (components: KadDHTComponents, init: TopologyListenerInit) { + super() + + const { protocol, lan } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:topology-listener:${lan ? 'lan' : 'wan'}`) + this.running = false + this.protocol = protocol + } + + isStarted (): boolean { + return this.running + } + + /** + * Start the network + */ + async start (): Promise { + if (this.running) { + return + } + + this.running = true + + // register protocol with topology + const topology = createTopology({ + onConnect: (peerId) => { + this.log('observed peer %p with protocol %s', peerId, this.protocol) + this.dispatchEvent(new CustomEvent('peer', { + detail: peerId + })) + } + }) + this.registrarId = await this.components.registrar.register(this.protocol, topology) + } + + /** + * Stop all network activity + */ + async stop (): Promise { + this.running = false + + // unregister protocol and handlers + if (this.registrarId != null) { + this.components.registrar.unregister(this.registrarId) + this.registrarId = undefined + } + } +} diff --git a/packages/kad-dht/src/utils.ts b/packages/kad-dht/src/utils.ts new file mode 100644 index 0000000000..dccf4c5b08 --- /dev/null +++ b/packages/kad-dht/src/utils.ts @@ -0,0 +1,151 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { Libp2pRecord } from '@libp2p/record' +import { Key } from 'interface-datastore/key' +import { sha256 } from 'multiformats/hashes/sha2' +import isPrivateIp from 'private-ip' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { RECORD_KEY_PREFIX } from './constants.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' + +// const IPNS_PREFIX = uint8ArrayFromString('/ipns/') +const PK_PREFIX = uint8ArrayFromString('/pk/') + +export function removePrivateAddresses (peer: PeerInfo): PeerInfo { + return { + ...peer, + multiaddrs: peer.multiaddrs.filter(multiaddr => { + const [[type, addr]] = multiaddr.stringTuples() + + // treat /dns, /dns4, and /dns6 addrs as public + if (type === 53 || type === 54 || type === 55) { + // localhost can be a dns address but it's private + if (addr === 'localhost') { + return false + } + + return true + } + + if (type !== 4 && type !== 6) { + return false + } + + if (addr == null) { + return false + } + + const isPrivate = isPrivateIp(addr) + + if (isPrivate == null) { + // not an ip address + return true + } + + return !isPrivate + }) + } +} + +export function removePublicAddresses (peer: PeerInfo): PeerInfo { + return { + ...peer, + multiaddrs: peer.multiaddrs.filter(multiaddr => { + const [[type, addr]] = multiaddr.stringTuples() + + if (addr === 'localhost') { + return true + } + + if (type !== 4 && type !== 6) { + return false + } + + if (addr == null) { + return false + } + + const isPrivate = isPrivateIp(addr) + + if (isPrivate == null) { + // not an ip address + return false + } + + return isPrivate + }) + } +} + +/** + * Creates a DHT ID by hashing a given Uint8Array + */ +export async function convertBuffer (buf: Uint8Array): Promise { + const multihash = await sha256.digest(buf) + + return multihash.digest +} + +/** + * Creates a DHT ID by hashing a Peer ID + */ +export async function convertPeerId (peerId: PeerId): Promise { + return convertBuffer(peerId.toBytes()) +} + +/** + * Convert a Uint8Array to their SHA2-256 hash + */ +export function bufferToKey (buf: Uint8Array): Key { + return new Key('/' + uint8ArrayToString(buf, 'base32'), false) +} + +/** + * Convert a Uint8Array to their SHA2-256 hash + */ +export function bufferToRecordKey (buf: Uint8Array): Key { + return new Key(`${RECORD_KEY_PREFIX}/${uint8ArrayToString(buf, 'base32')}`, false) +} + +/** + * Generate the key for a public key. + */ +export function keyForPublicKey (peer: PeerId): Uint8Array { + return uint8ArrayConcat([ + PK_PREFIX, + peer.toBytes() + ]) +} + +export function isPublicKeyKey (key: Uint8Array): boolean { + return uint8ArrayToString(key.subarray(0, 4)) === '/pk/' +} + +export function isIPNSKey (key: Uint8Array): boolean { + return uint8ArrayToString(key.subarray(0, 4)) === '/ipns/' +} + +export function fromPublicKeyKey (key: Uint8Array): PeerId { + return peerIdFromBytes(key.subarray(4)) +} + +/** + * Create a new put record, encodes and signs it if enabled + */ +export function createPutRecord (key: Uint8Array, value: Uint8Array): Uint8Array { + const timeReceived = new Date() + const rec = new Libp2pRecord(key, value, timeReceived) + + return rec.serialize() +} + +export function debounce (callback: () => void, wait: number = 100): () => void { + let timeout: ReturnType + + return (): void => { + clearTimeout(timeout) + timeout = setTimeout(() => { callback() }, wait) + } +} diff --git a/packages/kad-dht/test/enable-server-mode.spec.ts b/packages/kad-dht/test/enable-server-mode.spec.ts new file mode 100644 index 0000000000..80a93abdc1 --- /dev/null +++ b/packages/kad-dht/test/enable-server-mode.spec.ts @@ -0,0 +1,74 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import delay from 'delay' +import { TestDHT } from './utils/test-dht.js' + +const testCases: Array<[string, string, string]> = [ + ['should enable server mode when public IP4 addresses are found', '/ip4/139.178.91.71/udp/4001/quic', 'server'], + ['should enable server mode when public IP6 addresses are found', '/ip6/2604:1380:45e3:6e00::1/udp/4001/quic', 'server'], + ['should enable server mode when DNS4 addresses are found', '/dns4/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 'server'], + ['should enable server mode when DNS6 addresses are found', '/dns6/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 'server'], + ['should enable server mode when DNSADDR addresses are found', '/dnsaddr/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 'server'], + ['should not enable server mode when private IP4 addresses are found', '/ip4/127.0.0.1/udp/4001/quic', 'client'], + ['should not enable server mode when private IP6 addresses are found', '/ip6/::1/udp/4001/quic', 'client'], + ['should not enable server mode when otherwise public circuit relay addresses are found', '/dns4/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit', 'client'] +] + +describe('enable server mode', () => { + let tdht: TestDHT + + beforeEach(() => { + tdht = new TestDHT() + }) + + afterEach(async () => { + await tdht.teardown() + }) + + testCases.forEach(([name, addr, result]) => { + it(name, async function () { + const dht = await tdht.spawn() + + await expect(dht.getMode()).to.eventually.equal('client') + + dht.components.events.safeDispatchEvent('self:peer:update', { + detail: { + peer: { + addresses: [{ + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'), + isCertified: true + }, { + multiaddr: multiaddr('/ip6/::1/tcp/4001'), + isCertified: true + }, { + multiaddr: multiaddr(addr), + isCertified: true + }] + } + } + }) + + await delay(100) + + await expect(dht.getMode()).to.eventually.equal(result, `did not change to "${result}" mode after updating with address ${addr}`) + + dht.components.events.safeDispatchEvent('self:peer:update', { + detail: { + peer: { + addresses: [{ + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'), + isCertified: true + }] + } + } + }) + + await delay(100) + + await expect(dht.getMode()).to.eventually.equal('client', `did not reset to client mode after updating with address ${addr}`) + }) + }) +}) diff --git a/packages/kad-dht/test/fixtures/msg-1 b/packages/kad-dht/test/fixtures/msg-1 new file mode 100755 index 0000000000..85aadeee97 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-1 differ diff --git a/packages/kad-dht/test/fixtures/msg-2 b/packages/kad-dht/test/fixtures/msg-2 new file mode 100755 index 0000000000..3c75f5ed6c Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-2 differ diff --git a/packages/kad-dht/test/fixtures/msg-3 b/packages/kad-dht/test/fixtures/msg-3 new file mode 100755 index 0000000000..0821b20129 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-3 differ diff --git a/packages/kad-dht/test/fixtures/msg-4 b/packages/kad-dht/test/fixtures/msg-4 new file mode 100755 index 0000000000..de90e34463 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-4 differ diff --git a/packages/kad-dht/test/fixtures/msg-5 b/packages/kad-dht/test/fixtures/msg-5 new file mode 100755 index 0000000000..de90e34463 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-5 differ diff --git a/packages/kad-dht/test/fixtures/msg-6 b/packages/kad-dht/test/fixtures/msg-6 new file mode 100755 index 0000000000..de90e34463 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-6 differ diff --git a/packages/kad-dht/test/fixtures/msg-7 b/packages/kad-dht/test/fixtures/msg-7 new file mode 100755 index 0000000000..4044711de8 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-7 differ diff --git a/packages/kad-dht/test/fixtures/msg-8 b/packages/kad-dht/test/fixtures/msg-8 new file mode 100755 index 0000000000..4044711de8 Binary files /dev/null and b/packages/kad-dht/test/fixtures/msg-8 differ diff --git a/packages/kad-dht/test/generate-peers/.gitignore b/packages/kad-dht/test/generate-peers/.gitignore new file mode 100644 index 0000000000..8e593fee20 --- /dev/null +++ b/packages/kad-dht/test/generate-peers/.gitignore @@ -0,0 +1 @@ +generate-peer diff --git a/packages/kad-dht/test/generate-peers/generate-peer.go b/packages/kad-dht/test/generate-peers/generate-peer.go new file mode 100644 index 0000000000..129b5a44c9 --- /dev/null +++ b/packages/kad-dht/test/generate-peers/generate-peer.go @@ -0,0 +1,85 @@ +package main + +// this code has been extracted from https://github.com/libp2p/go-libp2p-kbucket/blob/b90e3fed3255e131058ac337a19beb2ad85da43f/table_refresh.go#L45 +// if the hash of that file changes it may need to be re-extracted + +import ( + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strconv" +) + +// maxCplForRefresh is the maximum cpl we support for refresh. +// This limit exists because we can only generate 'maxCplForRefresh' bit prefixes for now. +const maxCplForRefresh uint = 15 + +// GenRandPeerID generates a random peerID for a given Cpl +func GenRandPeerID(targetCpl uint, randPrefix uint16, localKadId []byte, keyPrefixMap []uint32) ([]byte, error) { + if targetCpl > maxCplForRefresh { + return nil, fmt.Errorf("cannot generate peer ID for Cpl greater than %d", maxCplForRefresh) + } + + localPrefix := binary.BigEndian.Uint16(localKadId) + + // For host with ID `L`, an ID `K` belongs to a bucket with ID `B` ONLY IF CommonPrefixLen(L,K) is EXACTLY B. + // Hence, to achieve a targetPrefix `T`, we must toggle the (T+1)th bit in L & then copy (T+1) bits from L + // to our randomly generated prefix. + toggledLocalPrefix := localPrefix ^ (uint16(0x8000) >> targetCpl) + + // Combine the toggled local prefix and the random bits at the correct offset + // such that ONLY the first `targetCpl` bits match the local ID. + mask := (^uint16(0)) << (16 - (targetCpl + 1)) + targetPrefix := (toggledLocalPrefix & mask) | (randPrefix & ^mask) + + // Convert to a known peer ID. + key := keyPrefixMap[targetPrefix] + + // mh.SHA2_256, peer-id-len + id := [34]byte{18, 32} + binary.BigEndian.PutUint32(id[2:], key) + return id[:], nil +} + +func main() { + jsonFile, err := os.Open("../../src/routing-table/generated-prefix-list.json") + if err != nil { + panic("Could not open generated prefix list") + } + + // defer the closing of our jsonFile so that we can parse it later on + defer jsonFile.Close() + + byteValue, err := ioutil.ReadAll(jsonFile) + if err != nil { + panic("Could not read generated prefix list") + } + + var keyPrefixMap []uint32 + json.Unmarshal([]byte(byteValue), &keyPrefixMap) + + targetCpl, err := strconv.ParseUint(os.Args[1], 10, 32) + if err != nil { + panic("Could not parse targetCpl") + } + + randPrefix, err := strconv.ParseUint(os.Args[2], 10, 16) + if err != nil { + panic("Could not parse randPrefix") + } + + localKadId, err := base64.StdEncoding.DecodeString(os.Args[3]) + if err != nil { + panic("Could not parse localKadId") + } + + b, err := GenRandPeerID(uint(targetCpl), uint16(randPrefix), localKadId, keyPrefixMap) + if err != nil { + panic("Could not generate peerId") + } + + fmt.Println(b) +} diff --git a/packages/kad-dht/test/generate-peers/generate-peers.node.ts b/packages/kad-dht/test/generate-peers/generate-peers.node.ts new file mode 100644 index 0000000000..f429d6534c --- /dev/null +++ b/packages/kad-dht/test/generate-peers/generate-peers.node.ts @@ -0,0 +1,101 @@ +/* eslint-env mocha */ +import path from 'path' +import { fileURLToPath } from 'url' +import { createRSAPeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { execa } from 'execa' +import { stubInterface } from 'ts-sinon' +import { toString as uintArrayToString } from 'uint8arrays/to-string' +import which from 'which' +import { RoutingTable } from '../../src/routing-table/index.js' +import { RoutingTableRefresh } from '../../src/routing-table/refresh.js' +import { + convertPeerId +} from '../../src/utils.js' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { PeerStore } from '@libp2p/interface-peer-store' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +async function fromGo (targetCpl: number, randPrefix: number, localKadId: string): Promise { + const { stdout } = await execa('./generate-peer', [targetCpl.toString(), randPrefix.toString(), localKadId], { + cwd: dirname + }) + + const arr = stdout + .slice(1, stdout.length - 1) + .split(' ') + .filter(Boolean) + .map(i => parseInt(i, 10)) + + return Uint8Array.from(arr) +} + +describe.skip('generate peers', function () { + this.timeout(540 * 1000) + const go = which.sync('go', { nothrow: true }) + + if (go == null) { + it.skip('No golang installation found on this system', () => {}) + + return + } + + let refresh: RoutingTableRefresh + + before(async () => { + await execa(go, ['build', 'generate-peer.go'], { + cwd: __dirname + }) + }) + + beforeEach(async () => { + const id = await createRSAPeerId({ bits: 512 }) + + const components = { + peerId: id, + connectionManager: stubInterface(), + peerStore: stubInterface() + } + const table = new RoutingTable(components, { + kBucketSize: 20, + lan: false, + protocol: '/ipfs/kad/1.0.0' + }) + refresh = new RoutingTableRefresh({ + routingTable: table, + // @ts-expect-error not a full implementation + peerRouting: {}, + lan: false + }) + }) + + const TEST_CASES = [{ + targetCpl: 2, + randPrefix: 29381 + }, { + targetCpl: 12, + randPrefix: 3821 + }, { + targetCpl: 5, + randPrefix: 9493 + }, { + targetCpl: 9, + randPrefix: 19209 + }, { + targetCpl: 1, + randPrefix: 49898 + }] + + TEST_CASES.forEach(({ targetCpl, randPrefix }) => { + it(`should generate peers targetCpl ${targetCpl} randPrefix ${randPrefix}`, async () => { + const peerId = await createRSAPeerId({ bits: 512 }) + const localKadId = await convertPeerId(peerId) + + const goOutput = await fromGo(targetCpl, randPrefix, uintArrayToString(localKadId, 'base64pad')) + const jsOutput = await refresh._makePeerId(localKadId, randPrefix, targetCpl) + + expect(goOutput).to.deep.equal(jsOutput) + }) + }) +}) diff --git a/packages/kad-dht/test/kad-dht.spec.ts b/packages/kad-dht/test/kad-dht.spec.ts new file mode 100644 index 0000000000..94b3687dc7 --- /dev/null +++ b/packages/kad-dht/test/kad-dht.spec.ts @@ -0,0 +1,889 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +import { CodeError } from '@libp2p/interfaces/errors' +import { Libp2pRecord } from '@libp2p/record' +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import drain from 'it-drain' +import filter from 'it-filter' +import last from 'it-last' +import map from 'it-map' +import { pipe } from 'it-pipe' +import sinon from 'sinon' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as c from '../src/constants.js' +import { EventTypes, type FinalPeerEvent, MessageType, type QueryEvent, type ValueEvent } from '../src/index.js' +import { MESSAGE_TYPE } from '../src/message/index.js' +import { peerResponseEvent } from '../src/query/events.js' +import * as kadUtils from '../src/utils.js' +import { createPeerIds } from './utils/create-peer-id.js' +import { createValues } from './utils/create-values.js' +import { countDiffPeers } from './utils/index.js' +import { sortClosestPeers } from './utils/sort-closest-peers.js' +import { TestDHT } from './utils/test-dht.js' +import type { DefaultDualKadDHT } from '../src/dual-kad-dht.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { CID } from 'multiformats/cid' + +async function findEvent (events: AsyncIterable, name: 'FINAL_PEER'): Promise +async function findEvent (events: AsyncIterable, name: 'VALUE'): Promise +async function findEvent (events: AsyncIterable, name: string): Promise { + const eventTypes = new Set() + + const event = await last( + filter(events, event => { + eventTypes.add(event.name) + return event.name === name + }) + ) + + if (event == null) { + throw new Error(`No ${name} event found, saw ${Array.from(eventTypes).join()}`) + } + + return event +} + +describe('KadDHT', () => { + let peerIds: PeerId[] + let values: Array<{ cid: CID, value: Uint8Array }> + let tdht: TestDHT + + beforeEach(() => { + tdht = new TestDHT() + }) + + afterEach(async () => { + await tdht.teardown() + }) + + before(async function () { + this.timeout(10 * 1000) + + const res = await Promise.all([ + createPeerIds(3), + createValues(20) + ]) + + peerIds = res[0] + values = res[1] + }) + + afterEach(() => { + sinon.restore() + }) + + describe('create', () => { + it('simple', async () => { + const dht = await tdht.spawn({ + kBucketSize: 5 + }) + + expect(dht).to.have.property('put') + expect(dht).to.have.property('get') + expect(dht).to.have.property('provide') + expect(dht).to.have.property('findProviders') + expect(dht).to.have.property('findPeer') + expect(dht).to.have.property('getClosestPeers') + expect(dht).to.have.property('getMode') + expect(dht).to.have.property('setMode') + }) + + it('forward providers init options to providers component', async () => { + const dht = await tdht.spawn({ + kBucketSize: 5, + providers: { + cleanupInterval: 60, + provideValidity: 60 * 10 + } + }) + expect(dht.lan.providers).to.have.property('cleanupInterval', 60) + expect(dht.lan.providers).to.have.property('provideValidity', 60 * 10) + expect(dht.wan.providers).to.have.property('cleanupInterval', 60) + expect(dht.wan.providers).to.have.property('provideValidity', 60 * 10) + }) + }) + + describe('start and stop', () => { + it('simple with defaults', async () => { + const dht = await tdht.spawn(undefined, false) + + sinon.spy(dht.wan.network, 'start') + sinon.spy(dht.wan.network, 'stop') + sinon.spy(dht.lan.network, 'start') + sinon.spy(dht.lan.network, 'stop') + + await dht.start() + expect(dht.wan.network.start).to.have.property('calledOnce', true) + expect(dht.lan.network.start).to.have.property('calledOnce', true) + + await dht.stop() + expect(dht.wan.network.stop).to.have.property('calledOnce', true) + expect(dht.lan.network.stop).to.have.property('calledOnce', true) + }) + + it('server mode', async () => { + // Currently off by default + const dht = await tdht.spawn(undefined, false) + + const registrarHandleSpy = sinon.spy(dht.components.registrar, 'handle') + + await dht.start() + // lan dht is always in server mode + expect(registrarHandleSpy).to.have.property('callCount', 1) + + await dht.setMode('server') + // now wan dht should be in server mode too + expect(registrarHandleSpy).to.have.property('callCount', 2) + + await dht.stop() + }) + + it('client mode', async () => { + // Currently on by default + const dht = await tdht.spawn({ clientMode: true }, false) + + const registrarHandleSpy = sinon.spy(dht.components.registrar, 'handle') + + await dht.start() + await dht.stop() + + // lan dht is always in server mode, wan is not + expect(registrarHandleSpy).to.have.property('callCount', 1) + }) + + it('should not fail when already started', async () => { + const dht = await tdht.spawn(undefined, false) + + await dht.start() + await dht.start() + await dht.start() + + await dht.stop() + }) + + it('should not fail to stop when was not started', async () => { + const dht = await tdht.spawn(undefined, false) + + await dht.stop() + }) + }) + + describe('content fetching', () => { + it('put - get same node', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const dht = await tdht.spawn() + + // Exchange data through the dht + await drain(dht.put(key, value)) + + const res = await last(dht.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + // Connect nodes + await tdht.connect(dhtA, dhtB) + + // Exchange data through the dht + await drain(dhtA.put(key, value)) + + const res = await findEvent(dhtB.get(key), 'VALUE') + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get calls progress handler', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + // Connect nodes + await tdht.connect(dhtA, dhtB) + + const putProgress = sinon.stub() + + // Exchange data through the dht + await drain(dhtA.put(key, value, { + onProgress: putProgress + })) + + expect(putProgress).to.have.property('called', true) + + const getProgress = sinon.stub() + + await drain(dhtB.get(key, { + onProgress: getProgress + })) + + expect(getProgress).to.have.property('called', true) + }) + + it('put - should require a minimum number of peers to have successful puts', async function () { + this.timeout(10 * 1000) + + const errCode = 'ERR_NOT_AVAILABLE' + const error = new CodeError('fake error', errCode) + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB, dhtC, dhtD] = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn({ + // Stub verify record + validators: { + v: sinon.stub().rejects(error) + } + }) + ]) + + await Promise.all([ + tdht.connect(dhtA, dhtB), + tdht.connect(dhtA, dhtC), + tdht.connect(dhtA, dhtD) + ]) + + // DHT operations + await drain(dhtA.put(key, value)) + + const res = await last(dhtB.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get using key with no prefix (no selector available)', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + await tdht.connect(dhtA, dhtB) + + // DHT operations + await drain(dhtA.put(key, value)) + + const res = await last(dhtB.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get using key from provided validator and selector', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/ipns/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn({ + validators: { + ipns: sinon.stub().resolves() + }, + selectors: { + ipns: sinon.stub().returns(0) + } + }), + tdht.spawn({ + validators: { + ipns: sinon.stub().resolves() + }, + selectors: { + ipns: sinon.stub().returns(0) + } + }) + ]) + + await tdht.connect(dhtA, dhtB) + + // DHT operations + await drain(dhtA.put(key, value)) + + const res = await last(dhtB.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get should fail if unrecognized key prefix in get', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v2/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + await tdht.connect(dhtA, dhtB) + + await drain(dhtA.put(key, value)) + + await expect(last(dhtA.get(key))).to.eventually.be.rejected().property('code', 'ERR_UNRECOGNIZED_KEY_PREFIX') + }) + + it('put - get with update', async function () { + this.timeout(20 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const valueA = uint8ArrayFromString('worldA') + const valueB = uint8ArrayFromString('worldB') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + const dhtASpy = sinon.spy(dhtA.lan.network, 'sendRequest') + + // Put before peers connected + await drain(dhtA.put(key, valueA)) + await drain(dhtB.put(key, valueB)) + + // Connect peers + await tdht.connect(dhtA, dhtB) + + // Get values + const resA = await last(dhtA.get(key)) + const resB = await last(dhtB.get(key)) + + // First is selected + expect(resA).to.have.property('value').that.equalBytes(valueA) + expect(resB).to.have.property('value').that.equalBytes(valueA) + + let foundGetValue = false + let foundPutValue = false + + for (const call of dhtASpy.getCalls()) { + if (call.args[0].equals(dhtB.components.peerId) && call.args[1].type === 'GET_VALUE') { + // query B + foundGetValue = true + } + + if (call.args[0].equals(dhtB.components.peerId) && call.args[1].type === 'PUT_VALUE') { + // update B + foundPutValue = true + } + } + + expect(foundGetValue).to.be.true('did not get value from dhtB') + expect(foundPutValue).to.be.true('did not update value on dhtB') + }) + + it('layered get', async function () { + this.timeout(40 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // Connect all + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + // DHT operations + await drain(dhts[3].put(key, value)) + + const res = await last(dhts[0].get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('getMany with nvals=1 goes out to swarm if there is no local value', async () => { + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + const rec = new Libp2pRecord(key, value, new Date()) + const dht = await tdht.spawn() + + // Simulate returning a peer id to query + sinon.stub(dht.lan.routingTable, 'closestPeers').returns([peerIds[1]]) + // Simulate going out to the network and returning the record + sinon.stub(dht.lan.peerRouting, 'getValueOrPeers').callsFake(async function * (peer) { + yield peerResponseEvent({ + messageType: MessageType.GET_VALUE, + from: peer, + record: rec + }) + }) // eslint-disable-line require-await + + const res = await last(dht.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + }) + + describe('content routing', () => { + it('provides', async function () { + this.timeout(20 * 1000) + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + const ids = dhts.map((d) => d.components.peerId) + const idsB58 = ids.map(id => id.toString()) + sinon.spy(dhts[3].lan.network, 'sendMessage') + + // connect peers + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + // provide values + await Promise.all(values.map(async (value) => { await drain(dhts[3].provide(value.cid)) })) + + // Expect an ADD_PROVIDER message to be sent to each peer for each value + const fn = dhts[3].lan.network.sendMessage + const valuesBuffs = values.map(v => v.cid.multihash.bytes) + // @ts-expect-error fn is a spy + const calls = fn.getCalls().map(c => c.args) + + for (const [peerId, msg] of calls) { + expect(idsB58).includes(peerId.toString()) + expect(msg.type).equals(MESSAGE_TYPE.ADD_PROVIDER) + expect(valuesBuffs).includes(msg.key) + expect(msg.providerPeers.length).equals(1) + expect(msg.providerPeers[0].id.toString()).equals(idsB58[3]) + } + + // Expect each DHT to find the provider of each value + let n = 0 + for (const v of values) { + n = (n + 1) % 3 + + const events = await all(dhts[n].findProviders(v.cid)) + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + + expect(provs).to.have.length(1) + expect(provs[0].toString()).to.equal(ids[3].toString()) + } + }) + + it('does not provide to wan if in client mode', async function () { + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // connect peers + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + const wanSpy = sinon.spy(dhts[0].wan, 'provide') + const lanSpy = sinon.spy(dhts[0].lan, 'provide') + + await drain(dhts[0].provide(values[0].cid)) + + expect(wanSpy.called).to.be.false() + expect(lanSpy.called).to.be.true() + }) + + it('provides to wan if in server mode', async function () { + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // connect peers + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + const wanSpy = sinon.spy(dhts[0].wan, 'provide') + const lanSpy = sinon.spy(dhts[0].lan, 'provide') + + await dhts[0].setMode('server') + + await drain(dhts[0].provide(values[0].cid)) + + expect(wanSpy.called).to.be.true() + expect(lanSpy.called).to.be.true() + }) + + it('find providers', async function () { + this.timeout(20 * 1000) + + const val = values[0] + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // Connect + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]) + ]) + + await Promise.all(dhts.map(async (dht) => { await drain(dht.provide(val.cid)) })) + + const events = await all(dhts[0].findProviders(val.cid)) + + // find providers find all the 3 providers + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(3) + }) + + it('find providers from client', async function () { + this.timeout(20 * 1000) + + const val = values[0] + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + const clientDHT = await tdht.spawn({ clientMode: true }) + + // Connect + await Promise.all([ + tdht.connect(clientDHT, dhts[0]), + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]) + ]) + + await Promise.all(dhts.map(async (dht) => { await drain(dht.provide(val.cid)) })) + + const events = await all(dhts[0].findProviders(val.cid)) + + // find providers find all the 3 providers + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(3) + }) + + it('find client provider', async function () { + this.timeout(20 * 1000) + + const val = values[0] + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + const clientDHT = await tdht.spawn({ clientMode: true }) + + // Connect + await Promise.all([ + tdht.connect(clientDHT, dhts[0]), + tdht.connect(dhts[0], dhts[1]) + ]) + + await drain(clientDHT.provide(val.cid)) + + await delay(1e3) + + const events = await all(dhts[1].findProviders(val.cid)) + + // find providers find the client provider + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(1) + }) + + it('find one provider locally', async function () { + this.timeout(20 * 1000) + const val = values[0] + + const dht = await tdht.spawn() + + sinon.stub(dht.components.peerStore, 'get').withArgs(dht.components.peerId) + .resolves({ + id: dht.components.peerId, + addresses: [], + protocols: [], + tags: new Map(), + metadata: new Map() + }) + sinon.stub(dht.lan.providers, 'getProviders').resolves([dht.components.peerId]) + + // Find provider + const events = await all(dht.findProviders(val.cid)) + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(1) + }) + }) + + describe('peer routing', () => { + it('findPeer', async function () { + this.timeout(240 * 1000) + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[0], dhts[2]), + tdht.connect(dhts[0], dhts[3]) + ]) + + const ids = dhts.map((d) => d.components.peerId) + + const finalPeer = await findEvent(dhts[0].findPeer(ids[ids.length - 1]), 'FINAL_PEER') + + expect(finalPeer.peer.id.equals(ids[ids.length - 1])).to.eql(true) + }) + + it('find peer query', async function () { + this.timeout(240 * 1000) + + // Create 101 nodes + const nDHTs = 101 + + const dhts = await Promise.all( + new Array(nDHTs).fill(0).map(async () => tdht.spawn()) + ) + + const dhtsById = new Map(dhts.map((d) => [d.components.peerId, d])) + const ids = [...dhtsById.keys()] + + // The origin node for the FIND_PEER query + const originNode = dhts[0] + + // The key + const val = uint8ArrayFromString('foobar') + + // Hash the key into the DHT's key format + const rtval = await kadUtils.convertBuffer(val) + // Make connections between nodes close to each other + const sorted = await sortClosestPeers(ids, rtval) + + const conns = [] + const maxRightIndex = sorted.length - 1 + for (let i = 0; i < sorted.length; i++) { + // Connect to 5 nodes on either side (10 in total) + for (const distance of [1, 3, 11, 31, 63]) { + let rightIndex = i + distance + if (rightIndex > maxRightIndex) { + rightIndex = maxRightIndex * 2 - (rightIndex + 1) + } + let leftIndex = i - distance + if (leftIndex < 0) { + leftIndex = 1 - leftIndex + } + conns.push([sorted[leftIndex], sorted[rightIndex]]) + } + } + + await Promise.all(conns.map(async (conn) => { + const dhtA = dhtsById.get(conn[0]) + const dhtB = dhtsById.get(conn[1]) + + if (dhtA == null || dhtB == null) { + throw new Error('Could not find DHT') + } + + await tdht.connect(dhtA, dhtB) + })) + + // Get the alpha (3) closest peers to the key from the origin's + // routing table + const rtablePeers = originNode.lan.routingTable.closestPeers(rtval, c.ALPHA) + expect(rtablePeers).to.have.length(c.ALPHA) + + // The set of peers used to initiate the query (the closest alpha + // peers to the key that the origin knows about) + const rtableSet: Record = {} + rtablePeers.forEach((p) => { + rtableSet[p.toString()] = true + }) + + const originNodeIndex = ids.findIndex(i => uint8ArrayEquals(i.multihash.bytes, originNode.components.peerId.multihash.bytes)) + const otherIds = ids.slice(0, originNodeIndex).concat(ids.slice(originNodeIndex + 1)) + + // Make the query + const out = await pipe( + originNode.getClosestPeers(val), + source => filter(source, (event) => event.type === EventTypes.FINAL_PEER), + // @ts-expect-error tsc has problems with filtering + source => map(source, (event) => event.peer.id), + async source => all(source) + ) + + const actualClosest = await sortClosestPeers(otherIds, rtval) + + // Expect that the response includes nodes that are were not + // already in the origin's routing table (ie it went out to + // the network to find closer peers) + expect(out.filter((p) => !rtableSet[p.toString()])) + .to.not.be.empty() + + // The expected closest kValue peers to the key + const exp = actualClosest.slice(0, c.K) + + // Expect the kValue peers found to include the kValue closest connected peers + // to the key + expect(countDiffPeers(out, exp)).to.equal(0) + }) + + it('getClosestPeers', async function () { + this.timeout(240 * 1000) + + const nDHTs = 30 + const dhts = await Promise.all( + new Array(nDHTs).fill(0).map(async () => tdht.spawn()) + ) + + const connected: Array> = [] + + for (let i = 0; i < dhts.length - 1; i++) { + connected.push(tdht.connect(dhts[i], dhts[(i + 1) % dhts.length])) + } + + await Promise.all(connected) + + const res = await all(filter(dhts[1].getClosestPeers(uint8ArrayFromString('foo')), event => event.name === 'FINAL_PEER')) + + expect(res).to.not.be.empty() + }) + }) + + describe('errors', () => { + it('get should fail if only has one peer', async function () { + this.timeout(240 * 1000) + + const dht = await tdht.spawn() + + await delay(100) + + await expect(all(dht.get(uint8ArrayFromString('/v/hello')))).to.eventually.be.rejected().property('code', 'ERR_NO_PEERS_IN_ROUTING_TABLE') + }) + + it('get should handle correctly an unexpected error', async function () { + this.timeout(240 * 1000) + + const errCode = 'ERR_INVALID_RECORD_FAKE' + const error = new CodeError('fake error', errCode) + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + await tdht.connect(dhtA, dhtB) + + const stub = sinon.stub(dhtA.components.connectionManager, 'openConnection').rejects(error) + + const errors = await all(filter(dhtA.get(uint8ArrayFromString('/v/hello')), event => event.name === 'QUERY_ERROR')) + + expect(errors).to.have.lengthOf(2) + expect(errors).to.have.nested.property('[0].error.code', errCode) + expect(errors).to.have.nested.property('[1].error.code', 'ERR_NOT_FOUND') + + stub.restore() + }) + + it('findPeer should fail if no closest peers available', async function () { + this.timeout(240 * 1000) + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + const ids = dhts.map((d) => d.components.peerId) + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + dhts[0].lan.findPeer = sinon.stub().returns([]) + dhts[0].wan.findPeer = sinon.stub().returns([]) + + await expect(drain(dhts[0].findPeer(ids[3]))).to.eventually.be.rejected().property('code', 'ERR_LOOKUP_FAILED') + }) + }) +}) diff --git a/packages/kad-dht/test/kad-utils.spec.ts b/packages/kad-dht/test/kad-utils.spec.ts new file mode 100644 index 0000000000..2d99cc4ebf --- /dev/null +++ b/packages/kad-dht/test/kad-utils.spec.ts @@ -0,0 +1,98 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import * as utils from '../src/utils.js' +import { createPeerId, createPeerIds } from './utils/create-peer-id.js' + +describe('kad utils', () => { + describe('bufferToKey', () => { + it('returns the base32 encoded key of the buffer', () => { + const buf = uint8ArrayFromString('hello world') + + const key = utils.bufferToKey(buf) + + expect(key.toString()) + .to.equal('/' + uint8ArrayToString(buf, 'base32')) + }) + }) + + describe('bufferToRecordKey', () => { + it('returns the base32 encoded key of the buffer with the record prefix', () => { + const buf = uint8ArrayFromString('hello world') + + const key = utils.bufferToRecordKey(buf) + + expect(key.toString()) + .to.equal('/dht/record/' + uint8ArrayToString(buf, 'base32')) + }) + }) + + describe('convertBuffer', () => { + it('returns the sha2-256 hash of the buffer', async () => { + const buf = uint8ArrayFromString('hello world') + const digest = await utils.convertBuffer(buf) + + expect(digest) + .to.equalBytes(uint8ArrayFromString('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 'base16')) + }) + }) + + describe('keyForPublicKey', () => { + it('works', async () => { + const peer = await createPeerId() + expect(utils.keyForPublicKey(peer)) + .to.eql(uint8ArrayConcat([uint8ArrayFromString('/pk/'), peer.multihash.bytes])) + }) + }) + + describe('fromPublicKeyKey', () => { + it('round trips', async function () { + this.timeout(40 * 1000) + + const peers = await createPeerIds(50) + peers.forEach((id, i) => { + expect(utils.isPublicKeyKey(utils.keyForPublicKey(id))).to.eql(true) + expect(utils.fromPublicKeyKey(utils.keyForPublicKey(id)).multihash.bytes) + .to.eql(id.multihash.bytes) + }) + }) + }) + + describe('removePrivateAddresses', () => { + it('filters private multiaddrs', async () => { + const id = await createPeerId() + + const multiaddrs = [ + multiaddr('/dns4/example.com/tcp/4001'), + multiaddr('/ip4/192.168.0.1/tcp/4001'), + multiaddr('/ip4/1.1.1.1/tcp/4001'), + multiaddr('/dns4/localhost/tcp/4001') + ] + + const peerInfo = utils.removePrivateAddresses({ id, multiaddrs, protocols: [] }) + expect(peerInfo.multiaddrs.map((ma) => ma.toString())) + .to.eql(['/dns4/example.com/tcp/4001', '/ip4/1.1.1.1/tcp/4001']) + }) + }) + + describe('removePublicAddresses', () => { + it('filters public multiaddrs', async () => { + const id = await createPeerId() + + const multiaddrs = [ + multiaddr('/dns4/example.com/tcp/4001'), + multiaddr('/ip4/192.168.0.1/tcp/4001'), + multiaddr('/ip4/1.1.1.1/tcp/4001'), + multiaddr('/dns4/localhost/tcp/4001') + ] + + const peerInfo = utils.removePublicAddresses({ id, multiaddrs, protocols: [] }) + expect(peerInfo.multiaddrs.map((ma) => ma.toString())) + .to.eql(['/ip4/192.168.0.1/tcp/4001', '/dns4/localhost/tcp/4001']) + }) + }) +}) diff --git a/packages/kad-dht/test/message.node.ts b/packages/kad-dht/test/message.node.ts new file mode 100644 index 0000000000..7154664498 --- /dev/null +++ b/packages/kad-dht/test/message.node.ts @@ -0,0 +1,31 @@ +/* eslint-env mocha */ + +import fs from 'fs' +import path from 'path' +import { isPeerId } from '@libp2p/interface-peer-id' +import { expect } from 'aegir/chai' +import range from 'lodash.range' +import { Message } from '../src/message/index.js' + +describe('Message', () => { + it('go-interop', () => { + range(1, 9).forEach((i) => { + const raw = fs.readFileSync( + path.join(process.cwd(), 'test', 'fixtures', `msg-${i}`) + ) + + const msg = Message.deserialize(raw) + + expect(msg.clusterLevel).to.gte(0) + if (msg.record != null) { + expect(msg.record.key).to.be.a('Uint8Array') + } + + if (msg.providerPeers.length > 0) { + msg.providerPeers.forEach((p) => { + expect(isPeerId(p.id)).to.be.true() + }) + } + }) + }) +}) diff --git a/packages/kad-dht/test/message.spec.ts b/packages/kad-dht/test/message.spec.ts new file mode 100644 index 0000000000..64cd22bc34 --- /dev/null +++ b/packages/kad-dht/test/message.spec.ts @@ -0,0 +1,86 @@ +/* eslint-env mocha */ + +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { Libp2pRecord } from '@libp2p/record' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import random from 'lodash.random' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../src/message/index.js' + +describe('Message', () => { + it('create', () => { + const k = uint8ArrayFromString('hello') + const msg = new Message(MESSAGE_TYPE.PING, k, 5) + + expect(msg).to.have.property('type', 'PING') + expect(msg).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(msg).to.have.property('clusterLevelRaw', 5) + expect(msg).to.have.property('clusterLevel', 4) + }) + + it('serialize & deserialize', async function () { + this.timeout(10 * 1000) + + const peers = await Promise.all( + Array.from({ length: 5 }).map(async () => createEd25519PeerId())) + + const closer = peers.slice(0, 5).map((p) => ({ + id: p, + multiaddrs: [ + multiaddr(`/ip4/198.176.1.${random(198)}/tcp/1234`), + multiaddr(`/ip4/100.176.1.${random(198)}`) + ], + protocols: [] + })) + + const provider = peers.slice(0, 5).map((p) => ({ + id: p, + multiaddrs: [ + multiaddr(`/ip4/98.176.1.${random(198)}/tcp/1234`), + multiaddr(`/ip4/10.176.1.${random(198)}`) + ], + protocols: [] + })) + + const msg = new Message(MESSAGE_TYPE.GET_VALUE, uint8ArrayFromString('hello'), 5) + const record = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), new Date()) + + msg.closerPeers = closer + msg.providerPeers = provider + msg.record = record + + const enc = msg.serialize() + const dec = Message.deserialize(enc) + + expect(dec.type).to.be.eql(msg.type) + expect(dec.key).to.be.eql(msg.key) + expect(dec.clusterLevel).to.be.eql(msg.clusterLevel) + + if (dec.record == null) { + throw new Error('No record found') + } + + expect(dec.record.serialize()).to.be.eql(record.serialize()) + expect(dec.record.key).to.eql(uint8ArrayFromString('hello')) + + expect(dec.closerPeers).to.have.length(5) + dec.closerPeers.forEach((peer, i) => { + expect(peer.id.equals(msg.closerPeers[i].id)).to.eql(true) + expect(peer.multiaddrs).to.eql(msg.closerPeers[i].multiaddrs) + }) + + expect(dec.providerPeers).to.have.length(5) + dec.providerPeers.forEach((peer, i) => { + expect(peer.id.equals(msg.providerPeers[i].id)).to.equal(true) + expect(peer.multiaddrs).to.eql(msg.providerPeers[i].multiaddrs) + }) + }) + + it('clusterlevel', () => { + const msg = new Message(MESSAGE_TYPE.PING, uint8ArrayFromString('hello'), 0) + + msg.clusterLevel = 10 + expect(msg.clusterLevel).to.eql(9) + }) +}) diff --git a/packages/kad-dht/test/multiple-nodes.spec.ts b/packages/kad-dht/test/multiple-nodes.spec.ts new file mode 100644 index 0000000000..24ecde0dd7 --- /dev/null +++ b/packages/kad-dht/test/multiple-nodes.spec.ts @@ -0,0 +1,108 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import drain from 'it-drain' +import last from 'it-last' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { TestDHT } from './utils/test-dht.js' +import type { DefaultDualKadDHT } from '../src/dual-kad-dht.js' + +describe('multiple nodes', function () { + this.timeout(60 * 1000) + const n = 8 + let tdht: TestDHT + let dhts: DefaultDualKadDHT[] + + // spawn nodes + beforeEach(async function () { + tdht = new TestDHT() + dhts = await Promise.all( + new Array(n).fill(0).map(async () => tdht.spawn({ + clientMode: false + })) + ) + + // all nodes except the last one + const range = Array.from(Array(n - 1).keys()) + + // connect the last one with the others one by one + return Promise.all(range.map(async (i) => { await tdht.connect(dhts[n - 1], dhts[i]) })) + }) + + afterEach(async function () { + await tdht.teardown() + }) + + it('put to "bootstrap" node and get with the others', async function () { + const key = uint8ArrayFromString('/v/hello0') + const value = uint8ArrayFromString('world') + + await drain(dhts[7].put(key, value)) + + const res = await Promise.all([ + last(dhts[0].get(key)), + last(dhts[1].get(key)), + last(dhts[2].get(key)), + last(dhts[3].get(key)), + last(dhts[4].get(key)), + last(dhts[5].get(key)), + last(dhts[6].get(key)) + ]) + + expect(res[0]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[1]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[2]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[3]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[4]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[5]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[6]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + }) + + it('put to a node and get with the others', async function () { + const key = uint8ArrayFromString('/v/hello1') + const value = uint8ArrayFromString('world') + + await drain(dhts[1].put(key, value)) + + const res = await Promise.all([ + last(dhts[0].get(key)), + last(dhts[2].get(key)), + last(dhts[3].get(key)), + last(dhts[4].get(key)), + last(dhts[5].get(key)), + last(dhts[6].get(key)), + last(dhts[7].get(key)) + ]) + + expect(res[0]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[1]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[2]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[3]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[4]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[5]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[6]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + }) + + it('put to several nodes in series with different values and get the last one in a subset of them', async function () { + const key = uint8ArrayFromString('/v/hallo') + const result = uint8ArrayFromString('world4') + + await drain(dhts[0].put(key, uint8ArrayFromString('world0'))) + await drain(dhts[1].put(key, uint8ArrayFromString('world1'))) + await drain(dhts[2].put(key, uint8ArrayFromString('world2'))) + await drain(dhts[3].put(key, uint8ArrayFromString('world3'))) + await drain(dhts[4].put(key, uint8ArrayFromString('world4'))) + + const res = await Promise.all([ + last(dhts[4].get(key)), + last(dhts[5].get(key)), + last(dhts[6].get(key)), + last(dhts[7].get(key)) + ]) + + expect(res[0]).have.property('value').that.equalBytes(result) + expect(res[1]).have.property('value').that.equalBytes(result) + expect(res[2]).have.property('value').that.equalBytes(result) + expect(res[3]).have.property('value').that.equalBytes(result) + }) +}) diff --git a/packages/kad-dht/test/network.spec.ts b/packages/kad-dht/test/network.spec.ts new file mode 100644 index 0000000000..6ab435fadc --- /dev/null +++ b/packages/kad-dht/test/network.spec.ts @@ -0,0 +1,113 @@ +/* eslint-env mocha */ + +import { mockStream } from '@libp2p/interface-mocks' +import { expect } from 'aegir/chai' +import all from 'it-all' +import * as lp from 'it-length-prefixed' +import map from 'it-map' +import { pipe } from 'it-pipe' +import pDefer from 'p-defer' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../src/message/index.js' +import { TestDHT } from './utils/test-dht.js' +import type { DefaultDualKadDHT } from '../src/dual-kad-dht.js' +import type { Connection } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Sink, Source } from 'it-stream-types' + +describe('Network', () => { + let dht: DefaultDualKadDHT + let tdht: TestDHT + + before(async function () { + this.timeout(10 * 1000) + tdht = new TestDHT() + dht = await tdht.spawn({ + clientMode: false + }) + }) + + after(async () => { await tdht.teardown() }) + + describe('sendRequest', () => { + it('send and response echo', async () => { + const msg = new Message(MESSAGE_TYPE.PING, uint8ArrayFromString('hello'), 0) + + const events = await all(dht.lan.network.sendRequest(dht.components.peerId, msg)) + const response = events + .filter(event => event.name === 'PEER_RESPONSE') + .pop() + expect(response).to.have.property('messageType', MESSAGE_TYPE.PING) + }) + + it('send and response different messages', async () => { + const defer = pDefer() + let i = 0 + const finish = (): void => { + if (i++ === 1) { + defer.resolve() + } + } + + const msg = new Message(MESSAGE_TYPE.PING, uint8ArrayFromString('hello'), 0) + + // mock it + dht.components.connectionManager.openConnection = async (peer: PeerId | Multiaddr | Multiaddr[]) => { + // @ts-expect-error incomplete implementation + const connection: Connection = { + newStream: async (protocols: string | string[]) => { + const protocol = Array.isArray(protocols) ? protocols[0] : protocols + const msg = new Message(MESSAGE_TYPE.FIND_NODE, uint8ArrayFromString('world'), 0) + + const data = await pipe( + [msg.serialize()], + (source) => lp.encode(source), + source => map(source, arr => new Uint8ArrayList(arr)), + async (source) => all(source) + ) + + const source = (async function * () { + const array = data + + yield * array + })() + + const sink: Sink, Promise> = async source => { + const res = await pipe( + source, + (source) => lp.decode(source), + async (source) => all(source) + ) + expect(Message.deserialize(res[0]).type).to.eql(MESSAGE_TYPE.PING) + finish() + } + + const stream = mockStream({ source, sink }) + + return { + ...stream, + stat: { + ...stream.stat, + protocol + } + } + } + } + + return connection + } + + const events = await all(dht.lan.network.sendRequest(dht.components.peerId, msg)) + const response = events + .filter(event => event.name === 'PEER_RESPONSE') + .pop() + + expect(response).to.have.property('messageType', MESSAGE_TYPE.FIND_NODE) + finish() + + return defer.promise + }) + }) +}) diff --git a/packages/kad-dht/test/peer-distance-list.spec.ts b/packages/kad-dht/test/peer-distance-list.spec.ts new file mode 100644 index 0000000000..06bc6ece8f --- /dev/null +++ b/packages/kad-dht/test/peer-distance-list.spec.ts @@ -0,0 +1,113 @@ +/* eslint-env mocha */ + +import { peerIdFromString } from '@libp2p/peer-id' +import { expect } from 'aegir/chai' +import { PeerDistanceList } from '../src/peer-list/peer-distance-list.js' +import * as kadUtils from '../src/utils.js' + +describe('PeerDistanceList', () => { + const p1 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1') + const p2 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2') + const p3 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE3') + const p4 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE4') + const p5 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1') + const p6 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE5') + const p7 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2') + + let key: Uint8Array + before(async () => { + key = await kadUtils.convertPeerId(p1) + }) + + describe('basics', () => { + it('add', async () => { + const pdl = new PeerDistanceList(key, 100) + + await pdl.add(p3) + await pdl.add(p1) + await pdl.add(p2) + await pdl.add(p4) + await pdl.add(p5) + await pdl.add(p1) + + // Note: p1 and p5 are equal + expect(pdl.length).to.eql(4) + expect(pdl.peers).to.be.eql([p1, p4, p3, p2]) + }) + + it('capacity', async () => { + const pdl = new PeerDistanceList(key, 3) + + await pdl.add(p1) + await pdl.add(p2) + await pdl.add(p3) + await pdl.add(p4) + await pdl.add(p5) + await pdl.add(p6) + + // Note: p1 and p5 are equal + expect(pdl.length).to.eql(3) + + // Closer peers added later should replace further + // peers added earlier + expect(pdl.peers).to.be.eql([p1, p4, p3]) + }) + }) + + describe('closer', () => { + let pdl: PeerDistanceList + + before(async () => { + pdl = new PeerDistanceList(key, 100) + + await pdl.add(p1) + await pdl.add(p2) + await pdl.add(p3) + await pdl.add(p4) + }) + + it('single closer peer', async () => { + const closer = await pdl.anyCloser([p6]) + + expect(closer).to.be.eql(true) + }) + + it('single further peer', async () => { + const closer = await pdl.anyCloser([p7]) + + expect(closer).to.be.eql(false) + }) + + it('closer and further peer', async () => { + const closer = await pdl.anyCloser([p6, p7]) + + expect(closer).to.be.eql(true) + }) + + it('single peer equal to furthest in list', async () => { + const closer = await pdl.anyCloser([p2]) + + expect(closer).to.be.eql(false) + }) + + it('no peers', async () => { + const closer = await pdl.anyCloser([]) + + expect(closer).to.be.eql(false) + }) + + it('empty peer distance list', async () => { + const pdl = new PeerDistanceList(key, 100) + const closer = await pdl.anyCloser([p1]) + + expect(closer).to.be.eql(true) + }) + + it('empty peer distance list and no peers', async () => { + const pdl = new PeerDistanceList(key, 100) + const closer = await pdl.anyCloser([]) + + expect(closer).to.be.eql(false) + }) + }) +}) diff --git a/packages/kad-dht/test/peer-list.spec.ts b/packages/kad-dht/test/peer-list.spec.ts new file mode 100644 index 0000000000..f2ebd2fecf --- /dev/null +++ b/packages/kad-dht/test/peer-list.spec.ts @@ -0,0 +1,26 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { PeerList } from '../src/peer-list/index.js' +import { createPeerIds } from './utils/create-peer-id.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('PeerList', () => { + let peers: PeerId[] + + before(async () => { + peers = await createPeerIds(3) + }) + + it('basics', () => { + const l = new PeerList() + + expect(l.push(peers[0])).to.eql(true) + expect(l.push(peers[0])).to.eql(false) + expect(l).to.have.length(1) + expect(l.push(peers[1])).to.eql(true) + expect(l.pop()).to.eql(peers[1]) + expect(l).to.have.length(1) + expect(l.toArray()).to.eql([peers[0]]) + }) +}) diff --git a/packages/kad-dht/test/providers.node.ts b/packages/kad-dht/test/providers.node.ts new file mode 100644 index 0000000000..934142165f --- /dev/null +++ b/packages/kad-dht/test/providers.node.ts @@ -0,0 +1,63 @@ +/* eslint-env mocha */ + +import os from 'os' +import path from 'path' +import { MemoryDatastore } from 'datastore-core/memory' +import { LevelDatastore } from 'datastore-level' +import { Providers } from '../src/providers.js' +import { createPeerIds } from './utils/create-peer-id.js' +import { createValues } from './utils/create-values.js' + +describe('Providers', () => { + let providers: Providers + + before(async function () { + this.timeout(10 * 1000) + }) + + afterEach(async () => { + await providers?.stop() + }) + + // slooow so only run when you need to + it.skip('many', async function () { + const p = path.join( + os.tmpdir(), (Math.random() * 100).toString() + ) + const store = new LevelDatastore(p) + await store.open() + providers = new Providers({ + datastore: new MemoryDatastore() + }, { + cacheSize: 10 + }) + + console.log('starting') // eslint-disable-line no-console + const [createdValues, createdPeers] = await Promise.all([ + createValues(100), + createPeerIds(600) + ]) + + console.log('got values and peers') // eslint-disable-line no-console + const total = Date.now() + + for (const v of createdValues) { + for (const p of createdPeers) { + await providers.addProvider(v.cid, p) + } + } + + console.log('addProvider %s peers %s cids in %sms', createdPeers.length, createdValues.length, Date.now() - total) // eslint-disable-line no-console + console.log('starting profile with %s peers and %s cids', createdPeers.length, createdValues.length) // eslint-disable-line no-console + + for (let i = 0; i < 3; i++) { + const start = Date.now() + for (const v of createdValues) { + await providers.getProviders(v.cid) + console.log('query %sms', (Date.now() - start)) // eslint-disable-line no-console + } + } + + await store.close() + }) +}) diff --git a/packages/kad-dht/test/providers.spec.ts b/packages/kad-dht/test/providers.spec.ts new file mode 100644 index 0000000000..84051bf8e0 --- /dev/null +++ b/packages/kad-dht/test/providers.spec.ts @@ -0,0 +1,113 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import delay from 'delay' +import { CID } from 'multiformats/cid' +import { sha256 } from 'multiformats/hashes/sha2' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Providers } from '../src/providers.js' +import { createPeerIds } from './utils/create-peer-id.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('Providers', () => { + let peers: PeerId[] + let providers: Providers + + before(async function () { + this.timeout(10 * 1000) + peers = await createPeerIds(3) + }) + + afterEach(async () => { + await providers?.stop() + }) + + it('simple add and get of providers', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }) + + const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + + await Promise.all([ + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[1]) + ]) + + const provs = await providers.getProviders(cid) + const ids = new Set(provs.map((peerId) => peerId.toString())) + expect(ids.has(peers[0].toString())).to.be.eql(true) + }) + + it('duplicate add of provider is deduped', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }) + + const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + + await Promise.all([ + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[1]), + providers.addProvider(cid, peers[1]), + providers.addProvider(cid, peers[1]) + ]) + + const provs = await providers.getProviders(cid) + expect(provs).to.have.length(2) + const ids = new Set(provs.map((peerId) => peerId.toString())) + expect(ids.has(peers[0].toString())).to.be.eql(true) + }) + + it('more providers than space in the lru cache', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }, { + cacheSize: 10 + }) + + const hashes = await Promise.all([...new Array(100)].map(async (i: number) => { + return sha256.digest(uint8ArrayFromString(`hello ${i}`)) + })) + + const cids = hashes.map((h) => CID.createV0(h)) + + await Promise.all(cids.map(async cid => { await providers.addProvider(cid, peers[0]) })) + const provs = await Promise.all(cids.map(async cid => providers.getProviders(cid))) + + expect(provs).to.have.length(100) + for (const p of provs) { + expect(p[0].toString()).to.be.equal(peers[0].toString()) + } + }) + + it('expires', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }, { + cleanupInterval: 100, + provideValidity: 200 + }) + await providers.start() + + const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + await Promise.all([ + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[1]) + ]) + + const provs = await providers.getProviders(cid) + + expect(provs).to.have.length(2) + expect(provs[0].toString()).to.be.equal(peers[0].toString()) + expect(provs[1].toString()).to.be.deep.equal(peers[1].toString()) + + await delay(400) + + const provsAfter = await providers.getProviders(cid) + expect(provsAfter).to.have.length(0) + await providers.stop() + }) +}) diff --git a/packages/kad-dht/test/query-self.spec.ts b/packages/kad-dht/test/query-self.spec.ts new file mode 100644 index 0000000000..2220419810 --- /dev/null +++ b/packages/kad-dht/test/query-self.spec.ts @@ -0,0 +1,128 @@ +/* eslint-env mocha */ + +import { CustomEvent } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import pDefer from 'p-defer' +import { stubInterface, type StubbedInstance } from 'ts-sinon' +import { finalPeerEvent } from '../src/query/events.js' +import { QuerySelf } from '../src/query-self.js' +import type { PeerRouting } from '../src/peer-routing/index.js' +import type { RoutingTable } from '../src/routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { DeferredPromise } from 'p-defer' + +describe('Query Self', () => { + let peerId: PeerId + let querySelf: QuerySelf + let peerRouting: StubbedInstance + let routingTable: StubbedInstance + let initialQuerySelfHasRun: DeferredPromise + + beforeEach(async () => { + peerId = await createEd25519PeerId() + initialQuerySelfHasRun = pDefer() + routingTable = stubInterface() + peerRouting = stubInterface() + + const components = { + peerId + } + + const init = { + lan: false, + peerRouting, + routingTable, + initialQuerySelfHasRun + } + + querySelf = new QuerySelf(components, init) + }) + + afterEach(() => { + if (querySelf != null) { + querySelf.stop() + } + }) + + it('should not run if not started', async () => { + await querySelf.querySelf() + + expect(peerRouting.getClosestPeers).to.have.property('callCount', 0) + }) + + it('should wait for routing table peers before running first query', async () => { + querySelf.start() + + // @ts-expect-error read-only property + routingTable.size = 0 + + const querySelfPromise = querySelf.querySelf() + const remotePeer = await createEd25519PeerId() + + let initialQuerySelfHasRunResolved = false + + void initialQuerySelfHasRun.promise.then(() => { + initialQuerySelfHasRunResolved = true + }) + + // should have registered a peer:add listener + // @ts-expect-error ts-sinon makes every property access a function and p-event checks this one first + expect(routingTable.on).to.have.property('callCount', 2) + // @ts-expect-error ts-sinon makes every property access a function and p-event checks this one first + expect(routingTable.on.getCall(0)).to.have.nested.property('args[0]', 'peer:add') + + // self query results + peerRouting.getClosestPeers.withArgs(peerId.toBytes()).returns(async function * () { + yield finalPeerEvent({ + from: remotePeer, + peer: { + id: remotePeer, + multiaddrs: [], + protocols: [] + } + }) + }()) + + // @ts-expect-error args[1] type could be an object + routingTable.on.getCall(0).args[1](new CustomEvent('peer:add', { detail: remotePeer })) + + // self-query should complete + await querySelfPromise + + // should have resolved initial query self promise + expect(initialQuerySelfHasRunResolved).to.be.true() + }) + + it('should join an existing query promise and not run twise', async () => { + querySelf.start() + + // @ts-expect-error read-only property + routingTable.size = 0 + + const querySelfPromise1 = querySelf.querySelf() + const querySelfPromise2 = querySelf.querySelf() + const remotePeer = await createEd25519PeerId() + + // self query results + peerRouting.getClosestPeers.withArgs(peerId.toBytes()).returns(async function * () { + yield finalPeerEvent({ + from: remotePeer, + peer: { + id: remotePeer, + multiaddrs: [], + protocols: [] + } + }) + }()) + + // @ts-expect-error args[1] type could be an object + routingTable.on.getCall(0).args[1](new CustomEvent('peer:add', { detail: remotePeer })) + + // both self-query promises should resolve + await Promise.all([querySelfPromise1, querySelfPromise2]) + + // should only have made one query + expect(peerRouting.getClosestPeers).to.have.property('callCount', 1) + }) +}) diff --git a/packages/kad-dht/test/query.spec.ts b/packages/kad-dht/test/query.spec.ts new file mode 100644 index 0000000000..7ffb678f80 --- /dev/null +++ b/packages/kad-dht/test/query.spec.ts @@ -0,0 +1,851 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import drain from 'it-drain' +import pDefer from 'p-defer' +import { type StubbedInstance, stubInterface } from 'ts-sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { EventTypes, type QueryEvent } from '../src/index.js' +import { MESSAGE_TYPE } from '../src/message/index.js' +import { + peerResponseEvent, + valueEvent, + queryErrorEvent +} from '../src/query/events.js' +import { QueryManager, type QueryManagerInit } from '../src/query/manager.js' +import { convertBuffer } from '../src/utils.js' +import { createPeerId, createPeerIds } from './utils/create-peer-id.js' +import { sortClosestPeers } from './utils/sort-closest-peers.js' +import type { QueryFunc } from '../src/query/types.js' +import type { RoutingTable } from '../src/routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +interface TopologyEntry { + delay?: number + error?: Error + value?: Uint8Array + closerPeers?: number[] + event: QueryEvent +} +type Topology = Record + +describe('QueryManager', () => { + let ourPeerId: PeerId + let peers: PeerId[] + let key: Uint8Array + let routingTable: StubbedInstance + + const defaultInit = (): QueryManagerInit => { + const init: QueryManagerInit = { + initialQuerySelfHasRun: pDefer(), + routingTable + } + + init.initialQuerySelfHasRun.resolve() + + return init + } + + function createTopology (opts: Record): Topology { + const topology: Record = {} + + Object.keys(opts).forEach(key => { + const id = parseInt(key) + const from = peers[id] + const config = opts[id] + + let event: QueryEvent + + if (config.value !== undefined) { + event = valueEvent({ from, value: config.value }) + } else if (config.error != null) { + event = queryErrorEvent({ from, error: config.error }) + } else { + event = peerResponseEvent({ + from, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: (config.closerPeers ?? []).map((id) => ({ + id: peers[id], + multiaddrs: [], + protocols: [] + })) + }) + } + + const entry: TopologyEntry = { + event + } + + if (config.delay != null) { + entry.delay = config.delay + } + + topology[from.toString()] = entry + }) + + return topology + } + + function createQueryFunction (topology: Record): QueryFunc { + const queryFunc: QueryFunc = async function * ({ peer }) { + const res = topology[peer.toString()] + + if (res.delay != null) { + await delay(res.delay) + } + + yield res.event + } + + return queryFunc + } + + before(async () => { + routingTable = stubInterface() + + const unsortedPeers = await createPeerIds(39) + ourPeerId = await createPeerId() + key = (await createPeerId()).toBytes() + + // sort remaining peers by XOR distance to the key, low -> high + peers = await sortClosestPeers(unsortedPeers, await convertBuffer(key)) + }) + + beforeEach(async () => { + routingTable.closestPeers.returns(peers) + }) + + it('does not run queries before start', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1 + }) + + // @ts-expect-error not enough params + await expect(all(manager.run())).to.eventually.be.rejectedWith(/not started/) + }) + + it('does not run queries after stop', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1 + }) + + await manager.start() + await manager.stop() + + // @ts-expect-error not enough params + await expect(all(manager.run())).to.eventually.be.rejectedWith(/not started/) + }) + + it('should pass query context', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * (context) { // eslint-disable-line require-await + expect(context).to.have.property('key').that.equalBytes(key) + expect(context).to.have.property('peer').that.deep.equals(peers[0]) + expect(context).to.have.property('signal').that.is.an.instanceOf(AbortSignal) + expect(context).to.have.property('pathIndex').that.equals(0) + expect(context).to.have.property('numPaths').that.equals(1) + + yield valueEvent({ + from: context.peer, + value: uint8ArrayFromString('cool') + }) + } + + const results = await all(manager.run(key, queryFunc)) + + expect(results).to.have.lengthOf(1) + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: uint8ArrayFromString('cool') + }]) + + await manager.stop() + }) + + it('simple run - succeed finding value', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + const peersQueried = [] + + const queryFunc: QueryFunc = async function * ({ peer, signal }) { // eslint-disable-line require-await + expect(signal).to.be.an.instanceOf(AbortSignal) + peersQueried.push(peer) + + if (peersQueried.length === 1) { + // query more peers + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: peers.slice(0, 5).map(id => ({ id, multiaddrs: [], protocols: [] })) + }) + } else if (peersQueried.length === 6) { + // all peers queried, return result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('cool') + }) + } else { + // a peer that cannot help in our query + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE + }) + } + } + + routingTable.closestPeers.returns([peers[7]]) + const results = await all(manager.run(key, queryFunc)) + + // e.g. our starting peer plus the 5x closerPeers returned n the first iteration + expect(results).to.have.lengthOf(6) + + expect(results).to.containSubset([{ + value: uint8ArrayFromString('cool') + }]) + // should be a result in there somewhere + + await manager.stop() + }) + + it('simple run - fail to find value', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + const peersQueried = [] + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + peersQueried.push(peer) + + if (peersQueried.length === 1) { + // query more peers + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: peers.slice(0, 5).map(id => ({ id, multiaddrs: [], protocols: [] })) + }) + } else { + // a peer that cannot help in our query + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE + }) + } + } + + routingTable.closestPeers.returns([peers[7]]) + const results = await all(manager.run(key, queryFunc)) + + // e.g. our starting peer plus the 5x closerPeers returned n the first iteration + expect(results).to.have.lengthOf(6) + // should not be a result in there + expect(results.find(res => res.name === 'VALUE')).to.not.be.ok() + + await manager.stop() + }) + + it('should abort a query', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2, + alpha: 1 + }) + await manager.start() + + const controller = new AbortController() + let aborted + + // 0 -> 10 -> 11 -> 12... + // 1 -> 20 -> 21 -> 22... + const topology = createTopology({ + 0: { closerPeers: [10] }, + 10: { closerPeers: [11] }, + 11: { closerPeers: [12] }, + 1: { closerPeers: [20] }, + 20: { closerPeers: [21] }, + 21: { closerPeers: [22] } + }) + + const queryFunc: QueryFunc = async function * ({ peer, signal }) { // eslint-disable-line require-await + signal.addEventListener('abort', () => { + aborted = true + }) + + await delay(1000) + + yield topology[peer.toString()].event + } + + setTimeout(() => { + controller.abort() + }, 10) + + await expect(all(manager.run(key, queryFunc, { signal: controller.signal }))).to.eventually.be.rejected().with.property('code', 'ERR_QUERY_ABORTED') + + expect(aborted).to.be.true() + + await manager.stop() + }) + + it('should allow a sub-query to timeout without aborting the whole query', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2, + alpha: 2 + }) + await manager.start() + + // 2 -> 1 -> 0 + // 4 -> 3 -> 0 + const topology = createTopology({ + 0: { value: uint8ArrayFromString('true') }, + 1: { delay: 1000, closerPeers: [0] }, + 2: { delay: 1000, closerPeers: [1] }, + 3: { delay: 10, closerPeers: [0] }, + 4: { delay: 10, closerPeers: [3] } + }) + + const queryFunc: QueryFunc = async function * ({ peer, signal }) { // eslint-disable-line require-await + let aborted = false + + signal.addEventListener('abort', () => { + aborted = true + }) + + const res = topology[peer.toString()] + + if (res.delay != null) { + await delay(res.delay) + } + + if (aborted) { + throw new Error('Aborted by signal') + } + + yield res.event + } + + routingTable.closestPeers.returns([peers[2], peers[4]]) + const result = await all(manager.run(key, queryFunc, { queryFuncTimeout: 500 })) + + // should have traversed through the three nodes to the value and the one that timed out + expect(result).to.have.lengthOf(4) + expect(result).to.have.deep.nested.property('[2].value', uint8ArrayFromString('true')) + expect(result).to.have.nested.property('[3].error.message', 'Aborted by signal') + + await manager.stop() + }) + + it('does not return an error if only some queries error', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 10 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer, pathIndex }) { // eslint-disable-line require-await + if (pathIndex % 2 === 0) { + yield queryErrorEvent({ + from: peer, + error: new Error('Urk!') + }) + } else { + yield peerResponseEvent({ from: peer, messageType: MESSAGE_TYPE.GET_VALUE }) + } + } + + const results = await all(manager.run(key, queryFunc)) + + // didn't add any extra peers during the query + expect(results).to.have.lengthOf(manager.disjointPaths) + // should not be a result in there + expect(results.find(res => res.name === 'VALUE')).to.not.be.ok() + // half of the results should have the error property + expect(results.reduce((acc, curr) => { + if (curr.name === 'QUERY_ERROR') { + return acc + 1 + } + + return acc + }, 0)).to.equal(5) + + await manager.stop() + }) + + it('returns empty run if initial peer list is empty', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 10 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + yield valueEvent({ from: peer, value: uint8ArrayFromString('cool') }) + } + + routingTable.closestPeers.returns([]) + const results = await all(manager.run(key, queryFunc)) + + expect(results).to.have.lengthOf(0) + + await manager.stop() + }) + + it('should query closer peers first', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + // 9 -> 8 -> 7 -> 6 -> 5 -> 0 + // \-> 4 -> 3 -> 2 -> 1 -> 0 <-- should take this branch first + const topology = createTopology({ + 9: { closerPeers: [8, 4] }, + 8: { closerPeers: [7] }, + 7: { closerPeers: [6] }, + 6: { closerPeers: [5] }, + 5: { closerPeers: [0] }, + 4: { closerPeers: [3] }, + 3: { closerPeers: [2] }, + 2: { closerPeers: [1] }, + 1: { closerPeers: [0] }, + 0: { value: uint8ArrayFromString('hello world') } + }) + + routingTable.closestPeers.returns([peers[9]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + const traversedPeers = results + .map(event => { + if (event.type !== EventTypes.PEER_RESPONSE && event.type !== EventTypes.VALUE) { + throw new Error(`Unexpected query event type ${event.type}`) + } + + return event.from + }) + + expect(traversedPeers).to.deep.equal([ + peers[9], + peers[4], + peers[3], + peers[2], + peers[1], + peers[0], + peers[8], + peers[7], + peers[6], + peers[5] + ]) + + await manager.stop() + }) + + it('should stop when passing through the same node twice', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 20, + alpha: 1 + }) + await manager.start() + + const topology = createTopology({ + 6: { closerPeers: [2] }, + 5: { closerPeers: [4] }, + 4: { closerPeers: [3] }, + 3: { closerPeers: [2] }, + 2: { closerPeers: [1] }, + 1: { closerPeers: [0] }, + 0: { value: uint8ArrayFromString('hello world') } + }) + + routingTable.closestPeers.returns([peers[6], peers[5]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + const traversedPeers = results + .map(event => { + if (event.type !== EventTypes.PEER_RESPONSE && event.type !== EventTypes.VALUE) { + throw new Error(`Unexpected query event type ${event.type}`) + } + + return event.from + }) + + expect(traversedPeers).lengthOf(7) + + await manager.stop() + }) + + it('only closerPeers', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: [{ + id: peers[2], + multiaddrs: [], + protocols: [] + }] + }) + } + + routingTable.closestPeers.returns([peers[3]]) + const results = await all(manager.run(key, queryFunc)) + + expect(results).to.have.lengthOf(2) + expect(results).to.have.deep.nested.property('[0].closer[0].id', peers[2]) + expect(results).to.have.deep.nested.property('[1].closer[0].id', peers[2]) + + await manager.stop() + }) + + it('only closerPeers concurrent', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 3 + }) + await manager.start() + + // 9 -> 2 + // 8 -> 6 -> 4 + // 5 -> 3 + // 7 -> 1 -> 0 + const topology = createTopology({ + 0: { closerPeers: [] }, + 1: { closerPeers: [0] }, + 2: { closerPeers: [] }, + 3: { closerPeers: [] }, + 4: { closerPeers: [] }, + 5: { closerPeers: [3] }, + 6: { closerPeers: [4, 5] }, + 7: { closerPeers: [1] }, + 8: { closerPeers: [6] }, + 9: { closerPeers: [2] } + }) + + routingTable.closestPeers.returns([peers[9], peers[8], peers[7]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // Should visit all peers + expect(results).to.have.lengthOf(10) + + await manager.stop() + }) + + it('queries stop after shutdown', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + // 3 -> 2 -> 1 -> 0 + const topology = createTopology({ + 0: { closerPeers: [] }, + // Should not reach here because query gets shut down + 1: { closerPeers: [0] }, + 2: { closerPeers: [1] }, + 3: { closerPeers: [2] } + }) + + const visited: PeerId[] = [] + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + visited.push(peer) + + const getResult = async (): Promise => { + const res = topology[peer.toString()] + // this delay is necessary so `dhtA.stop` has time to stop the + // requests before they all complete + await delay(100) + + return res.event + } + + // Shut down after visiting peers[2] + if (peer === peers[2]) { + await manager.stop() + + yield getResult() + } + + yield getResult() + } + + // shutdown will cause the query to stop early but without an error + routingTable.closestPeers.returns([peers[3]]) + await drain(manager.run(key, queryFunc)) + + // Should only visit peers up to the point where we shut down + expect(visited).to.have.lengthOf(2) + expect(visited).to.deep.include(peers[3]) + expect(visited).to.deep.include(peers[2]) + }) + + it('disjoint path values', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2 + }) + await manager.start() + + const values = ['v0', 'v1'].map((str) => uint8ArrayFromString(str)) + + // 2 -> 1 -> 0 (v0) + // 4 -> 3 (v1) + const topology = createTopology({ + 0: { value: values[0] }, + // Top level node + 1: { closerPeers: [0] }, + 2: { closerPeers: [1] }, + 3: { value: values[1] }, + 4: { closerPeers: [3] } + }) + + routingTable.closestPeers.returns([peers[2], peers[4]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // visited all the nodes + expect(results).to.have.lengthOf(5) + + // found both values + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: values[0] + }]) + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: values[1] + }]) + + await manager.stop() + }) + + it('disjoint path continue other paths after error on one path', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2 + }) + await manager.start() + + // 2 -> 1 (delay) -> 0 [pathComplete] + // 5 -> 4 [error] -> 3 + const topology = createTopology({ + 0: { value: uint8ArrayFromString('true') }, + // This query has a delay which means it only returns after the other + // path has already returned an error + 1: { delay: 100, closerPeers: [0] }, + 2: { closerPeers: [1] }, + 3: { value: uint8ArrayFromString('false') }, + // Return an error at this point + 4: { closerPeers: [3], error: new Error('Nooo!') }, + 5: { closerPeers: [4] } + }) + + routingTable.closestPeers.returns([peers[2], peers[5]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: uint8ArrayFromString('true') + }]) + // @ts-expect-error types are wrong + expect(results).to.not.deep.containSubset([{ + value: uint8ArrayFromString('false') + }]) + + await manager.stop() + }) + + it('should allow the self-query query to run', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + initialQuerySelfHasRun: pDefer(), + routingTable + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + // yield query result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('cool') + }) + } + + routingTable.closestPeers.returns([peers[7]]) + const results = await all(manager.run(key, queryFunc, { + // this bypasses awaiting on the initialQuerySelfHasRun deferred promise + isSelfQuery: true + })) + + // should have the result + expect(results).to.containSubset([{ + value: uint8ArrayFromString('cool') + }]) + + await manager.stop() + }) + + it('should wait for the self-query query to run before running other queries', async () => { + const initialQuerySelfHasRun = pDefer() + + const manager = new QueryManager({ + peerId: ourPeerId + }, { + initialQuerySelfHasRun, + alpha: 2, + routingTable + }) + await manager.start() + + let regularQueryTimeStarted: number = 0 + let selfQueryTimeStarted: number = Infinity + + routingTable.closestPeers.returns([peers[7]]) + + // run a regular query and the self query together + await Promise.all([ + all(manager.run(key, async function * ({ peer }) { // eslint-disable-line require-await + regularQueryTimeStarted = Date.now() + + // yield query result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('cool') + }) + })), + all(manager.run(key, async function * ({ peer }) { // eslint-disable-line require-await + selfQueryTimeStarted = Date.now() + + // make sure we take enough time so that the `regularQuery` time diff is big enough to measure + await delay(100) + + // yield query result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('it me') + }) + + // normally done by the QuerySelf component + initialQuerySelfHasRun.resolve() + }, { + // this bypasses awaiting on the initialQuerySelfHasRun deferred promise + isSelfQuery: true + })) + ]) + + // should have started the regular query after the self query finished + expect(regularQueryTimeStarted).to.be.greaterThan(selfQueryTimeStarted) + + await manager.stop() + }) + + it('should end paths when they have no closer peers to those already queried', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + // 3 -> 2 -> 1 -> 4 -> 5 -> 6 // should stop at 1 + const topology = createTopology({ + 1: { closerPeers: [4] }, + 2: { closerPeers: [1] }, + 3: { closerPeers: [2] }, + 4: { closerPeers: [5] }, + 5: { closerPeers: [6] }, + 6: {} + }) + + routingTable.closestPeers.returns([peers[3]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // should not have a value + expect(results.find(res => res.name === 'VALUE')).to.not.be.ok() + + // should have traversed peers 3, 2 & 1 + expect(results).to.containSubset([{ + from: peers[3] + }, { + from: peers[2] + }, { + from: peers[1] + }]) + + // should not have traversed peers 4, 5 & 6 + expect(results).to.not.containSubset([{ + from: peers[4] + }, { + from: peers[5] + }, { + from: peers[6] + }]) + + await manager.stop() + }) +}) diff --git a/packages/kad-dht/test/routing-table.spec.ts b/packages/kad-dht/test/routing-table.spec.ts new file mode 100644 index 0000000000..5fafc29784 --- /dev/null +++ b/packages/kad-dht/test/routing-table.spec.ts @@ -0,0 +1,345 @@ +/* eslint-env mocha */ + +import { mockConnectionManager } from '@libp2p/interface-mocks' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { PeerSet } from '@libp2p/peer-collections' +import { peerIdFromString } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import all from 'it-all' +import { pipe } from 'it-pipe' +import random from 'lodash.random' +import { pEvent } from 'p-event' +import pWaitFor from 'p-wait-for' +import sinon from 'sinon' +import { stubInterface } from 'ts-sinon' +import { PROTOCOL_DHT } from '../src/constants.js' +import { KAD_CLOSE_TAG_NAME, KAD_CLOSE_TAG_VALUE, KBUCKET_SIZE, RoutingTable, type RoutingTableComponents } from '../src/routing-table/index.js' +import * as kadUtils from '../src/utils.js' +import { createPeerId, createPeerIds } from './utils/create-peer-id.js' +import { sortClosestPeers } from './utils/sort-closest-peers.js' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Registrar } from '@libp2p/interface-registrar' + +describe('Routing Table', () => { + let table: RoutingTable + let components: RoutingTableComponents + + beforeEach(async function () { + this.timeout(20 * 1000) + + const events = new EventEmitter() + + components = { + peerId: await createPeerId(), + connectionManager: stubInterface(), + peerStore: stubInterface() + } + components.connectionManager = mockConnectionManager({ + ...components, + registrar: stubInterface(), + events + }) + components.peerStore = new PersistentPeerStore({ + ...components, + datastore: new MemoryDatastore(), + events + }) + + table = new RoutingTable(components, { + lan: false, + protocol: PROTOCOL_DHT + }) + await table.start() + }) + + afterEach(async () => { + await table.stop() + }) + + it('add', async function () { + this.timeout(20 * 1000) + + const ids = await createPeerIds(20) + + await Promise.all( + Array.from({ length: 1000 }).map(async () => { await table.add(ids[random(ids.length - 1)]) }) + ) + + await Promise.all( + Array.from({ length: 20 }).map(async () => { + const id = ids[random(ids.length - 1)] + const key = await kadUtils.convertPeerId(id) + + expect(table.closestPeers(key, 5).length) + .to.be.above(0) + }) + ) + }) + + it('emits peer:add event', async () => { + const id = await createEd25519PeerId() + const eventPromise = pEvent<'peer:add', CustomEvent>(table, 'peer:add') + + await table.add(id) + + const event = await eventPromise + expect(event.detail.toString()).to.equal(id.toString()) + }) + + it('remove', async function () { + this.timeout(20 * 1000) + + const peers = await createPeerIds(10) + await Promise.all(peers.map(async (peer) => { await table.add(peer) })) + + const key = await kadUtils.convertPeerId(peers[2]) + expect(table.closestPeers(key, 10)).to.have.length(10) + + await table.remove(peers[5]) + expect(table.closestPeers(key, 10)).to.have.length(9) + expect(table.size).to.be.eql(9) + }) + + it('emits peer:remove event', async () => { + const id = await createEd25519PeerId() + const eventPromise = pEvent<'peer:remove', CustomEvent>(table, 'peer:remove') + + await table.add(id) + await table.remove(id) + + const event = await eventPromise + expect(event.detail.toString()).to.equal(id.toString()) + }) + + it('closestPeer', async function () { + this.timeout(10 * 1000) + + const peers = await createPeerIds(4) + await Promise.all(peers.map(async (peer) => { await table.add(peer) })) + + const id = peers[2] + const key = await kadUtils.convertPeerId(id) + expect(table.closestPeer(key)).to.eql(id) + }) + + it('closestPeers', async function () { + this.timeout(20 * 1000) + + const peers = await createPeerIds(18) + await Promise.all(peers.map(async (peer) => { await table.add(peer) })) + + const key = await kadUtils.convertPeerId(peers[2]) + expect(table.closestPeers(key, 15)).to.have.length(15) + }) + + it('favours old peers that respond to pings', async () => { + let fn: (() => Promise) | undefined + + // execute queued functions immediately + // @ts-expect-error incomplete implementation + table.pingQueue = { + add: async (f: () => Promise) => { + fn = f + }, + clear: () => {} + } + + const peerIds = [ + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi5'), + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi6') + ] + + const oldPeer = { + id: peerIds[0].toBytes(), + peer: peerIds[0] + } + const newPeer = { + id: peerIds[1].toBytes(), + peer: peerIds[1] + } + + table._onPing(new CustomEvent('ping', { detail: { oldContacts: [oldPeer], newContact: newPeer } })) + + if (table.kb == null) { + throw new Error('kbucket not defined') + } + + // add the old peer + table.kb.add(oldPeer) + + // simulate connection succeeding + const newStreamStub = sinon.stub().withArgs(PROTOCOL_DHT).resolves({ close: sinon.stub() }) + const openConnectionStub = sinon.stub().withArgs(oldPeer.peer).resolves({ + newStream: newStreamStub + }) + components.connectionManager.openConnection = openConnectionStub + + if (fn == null) { + throw new Error('nothing added to queue') + } + + // perform the ping + await fn() + + expect(openConnectionStub.calledOnce).to.be.true() + expect(openConnectionStub.calledWith(oldPeer.peer)).to.be.true() + + expect(newStreamStub.callCount).to.equal(1) + expect(newStreamStub.calledWith(PROTOCOL_DHT)).to.be.true() + + // did not add the new peer + expect(table.kb.get(newPeer.id)).to.be.undefined() + + // kept the old peer + expect(table.kb.get(oldPeer.id)).to.not.be.undefined() + }) + + it('evicts oldest peer that does not respond to ping', async () => { + let fn: (() => Promise) | undefined + + // execute queued functions immediately + // @ts-expect-error incomplete implementation + table.pingQueue = { + add: async (f: () => Promise) => { + fn = f + }, + clear: () => {} + } + + const peerIds = [ + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi5'), + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi6') + ] + + const oldPeer = { + id: peerIds[0].toBytes(), + peer: peerIds[0] + } + const newPeer = { + id: peerIds[1].toBytes(), + peer: peerIds[1] + } + + table._onPing(new CustomEvent('ping', { detail: { oldContacts: [oldPeer], newContact: newPeer } })) + + if (table.kb == null) { + throw new Error('kbucket not defined') + } + + // add the old peer + table.kb.add(oldPeer) + + // libp2p fails to dial the old peer + const openConnectionStub = sinon.stub().withArgs(oldPeer.peer).rejects(new Error('Could not dial peer')) + components.connectionManager.openConnection = openConnectionStub + + if (fn == null) { + throw new Error('nothing added to queue') + } + + // perform the ping + await fn() + + expect(openConnectionStub.callCount).to.equal(1) + expect(openConnectionStub.calledWith(oldPeer.peer)).to.be.true() + + // added the new peer + expect(table.kb.get(newPeer.id)).to.not.be.undefined() + + // evicted the old peer + expect(table.kb.get(oldPeer.id)).to.be.undefined() + }) + + it('tags newly found kad-close peers', async () => { + const remotePeer = await createEd25519PeerId() + const tagPeerSpy = sinon.spy(components.peerStore, 'merge') + + await table.add(remotePeer) + + expect(tagPeerSpy.callCount).to.equal(0, 'did not debounce call to peerStore.tagPeer') + + await pWaitFor(() => { + return tagPeerSpy.callCount === 1 + }) + + expect(tagPeerSpy.callCount).to.equal(1, 'did not tag kad-close peer') + expect(tagPeerSpy.getCall(0).args[0].toString()).to.equal(remotePeer.toString()) + expect(tagPeerSpy.getCall(0).args[1].tags).to.deep.equal({ + [KAD_CLOSE_TAG_NAME]: { + value: KAD_CLOSE_TAG_VALUE + } + }) + }) + + it('removes tags from kad-close peers when closer peers are found', async () => { + async function getTaggedPeers (): Promise { + return new PeerSet(await pipe( + await components.peerStore.all(), + async function * (source) { + for await (const peer of source) { + const peerData = await components.peerStore.get(peer.id) + + if (peerData.tags.has(KAD_CLOSE_TAG_NAME)) { + yield peer.id + } + } + }, + async (source) => all(source) + )) + } + + const tagPeerSpy = sinon.spy(components.peerStore, 'merge') + const localNodeId = await kadUtils.convertPeerId(components.peerId) + const sortedPeerList = await sortClosestPeers( + await Promise.all( + new Array(KBUCKET_SIZE + 1).fill(0).map(async () => createEd25519PeerId()) + ), + localNodeId + ) + + // sort list furthest -> closest + sortedPeerList.reverse() + + // fill the table up to the first kbucket size + for (let i = 0; i < KBUCKET_SIZE; i++) { + await table.add(sortedPeerList[i]) + } + + // should have all added contacts in the root kbucket + expect(table.kb?.count()).to.equal(KBUCKET_SIZE, 'did not fill kbuckets') + expect(table.kb?.root.contacts).to.have.lengthOf(KBUCKET_SIZE, 'split root kbucket when we should not have') + expect(table.kb?.root.left).to.be.null('split root kbucket when we should not have') + expect(table.kb?.root.right).to.be.null('split root kbucket when we should not have') + + await pWaitFor(() => { + return tagPeerSpy.callCount === KBUCKET_SIZE + }) + + // make sure we tagged all of the peers as kad-close + const taggedPeers = await getTaggedPeers() + expect(taggedPeers.difference(new PeerSet(sortedPeerList.slice(0, sortedPeerList.length - 1)))).to.have.property('size', 0) + tagPeerSpy.resetHistory() + + // add a node that is closer than any added so far + await table.add(sortedPeerList[sortedPeerList.length - 1]) + + expect(table.kb?.count()).to.equal(KBUCKET_SIZE + 1, 'did not fill kbuckets') + expect(table.kb?.root.left).to.not.be.null('did not split root kbucket when we should have') + expect(table.kb?.root.right).to.not.be.null('did not split root kbucket when we should have') + + // wait for tag new peer and untag old peer + await pWaitFor(() => { + return tagPeerSpy.callCount === 2 + }) + + // should have updated list of tagged peers + const finalTaggedPeers = await getTaggedPeers() + expect(finalTaggedPeers.difference(new PeerSet(sortedPeerList.slice(1)))).to.have.property('size', 0) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts new file mode 100644 index 0000000000..3455ce5e70 --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts @@ -0,0 +1,85 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { Providers } from '../../../src/providers.js' +import { AddProviderHandler } from '../../../src/rpc/handlers/add-provider.js' +import { createPeerIds } from '../../utils/create-peer-id.js' +import { createValues } from '../../utils/create-values.js' +import type { DHTMessageHandler } from '../../../src/rpc/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { CID } from 'multiformats' + +describe('rpc - handlers - AddProvider', () => { + let peerIds: PeerId[] + let values: Array<{ cid: CID, value: Uint8Array }> + let handler: DHTMessageHandler + let providers: Providers + + before(async () => { + [peerIds, values] = await Promise.all([ + createPeerIds(3), + createValues(2) + ]) + }) + + beforeEach(async () => { + const datastore = new MemoryDatastore() + + providers = new Providers({ datastore }) + + handler = new AddProviderHandler({ + providers + }) + }) + + describe('invalid messages', () => { + const tests = [{ + message: new Message(MESSAGE_TYPE.ADD_PROVIDER, new Uint8Array(0), 0), + error: 'ERR_MISSING_KEY' + }, { + message: new Message(MESSAGE_TYPE.ADD_PROVIDER, uint8ArrayFromString('hello world'), 0), + error: 'ERR_INVALID_CID' + }] + + tests.forEach((t) => { + it(t.error.toString(), async () => { + try { + await handler.handle(peerIds[0], t.message) + } catch (err: any) { + expect(err).to.exist() + expect(err.code).to.equal(t.error) + return + } + throw new Error() + }) + }) + }) + + it('ignore providers that do not match the sender', async () => { + const cid = values[0].cid + const msg = new Message(MESSAGE_TYPE.ADD_PROVIDER, cid.bytes, 0) + + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/1234') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/2345') + + msg.providerPeers = [{ + id: peerIds[0], + multiaddrs: [ma1], + protocols: [] + }, { + id: peerIds[1], + multiaddrs: [ma2], + protocols: [] + }] + + await handler.handle(peerIds[0], msg) + + const provs = await providers.getProviders(cid) + expect(provs).to.have.length(1) + expect(provs[0].toString()).to.equal(peerIds[0].toString()) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/find-node.spec.ts b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts new file mode 100644 index 0000000000..98ec8e84b0 --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts @@ -0,0 +1,166 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import Sinon, { type SinonStubbedInstance } from 'sinon' +import { stubInterface } from 'ts-sinon' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PeerRouting } from '../../../src/peer-routing/index.js' +import { FindNodeHandler } from '../../../src/rpc/handlers/find-node.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { DHTMessageHandler } from '../../../src/rpc/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { StubbedInstance } from 'ts-sinon' + +const T = MESSAGE_TYPE.FIND_NODE + +describe('rpc - handlers - FindNode', () => { + let peerId: PeerId + let sourcePeer: PeerId + let targetPeer: PeerId + let handler: DHTMessageHandler + let peerRouting: SinonStubbedInstance + let addressManager: StubbedInstance + + beforeEach(async () => { + peerId = await createPeerId() + sourcePeer = await createPeerId() + targetPeer = await createPeerId() + peerRouting = Sinon.createStubInstance(PeerRouting) + addressManager = stubInterface() + + handler = new FindNodeHandler({ + peerId, + addressManager + }, { + peerRouting, + lan: false + }) + }) + + it('returns self, if asked for self', async () => { + const msg = new Message(T, peerId.multihash.bytes, 0) + + addressManager.getAddresses.returns([ + multiaddr(`/ip4/127.0.0.1/tcp/4002/p2p/${peerId.toString()}`), + multiaddr(`/ip4/192.168.1.5/tcp/4002/p2p/${peerId.toString()}`), + multiaddr(`/ip4/221.4.67.0/tcp/4002/p2p/${peerId.toString()}`) + ]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(peerId) + }) + + it('returns closer peers', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline + .withArgs(targetPeer.multihash.bytes, sourcePeer) + .resolves([{ + id: targetPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/221.4.67.0/tcp/4002') + ], + protocols: [] + }]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(targetPeer) + expect(peer.multiaddrs).to.not.be.empty() + }) + + it('handles no peers found', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline.resolves([]) + + const response = await handler.handle(sourcePeer, msg) + + expect(response).to.have.property('closerPeers').that.is.empty() + }) + + it('returns only lan addresses', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline + .withArgs(targetPeer.multihash.bytes, sourcePeer) + .resolves([{ + id: targetPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/221.4.67.0/tcp/4002') + ], + protocols: [] + }]) + + handler = new FindNodeHandler({ + peerId, + addressManager + }, { + peerRouting, + lan: true + }) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(targetPeer) + expect(peer.multiaddrs.map(ma => ma.toString())).to.include('/ip4/192.168.1.5/tcp/4002') + expect(peer.multiaddrs.map(ma => ma.toString())).to.not.include('/ip4/221.4.67.0/tcp/4002') + }) + + it('returns only wan addresses', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline + .withArgs(targetPeer.multihash.bytes, sourcePeer) + .resolves([{ + id: targetPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/221.4.67.0/tcp/4002') + ], + protocols: [] + }]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(targetPeer) + expect(peer.multiaddrs.map(ma => ma.toString())).to.not.include('/ip4/192.168.1.5/tcp/4002') + expect(peer.multiaddrs.map(ma => ma.toString())).to.include('/ip4/221.4.67.0/tcp/4002') + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts new file mode 100644 index 0000000000..49feba7fcc --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts @@ -0,0 +1,112 @@ +/* eslint-env mocha */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import Sinon, { type SinonStubbedInstance } from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PeerRouting } from '../../../src/peer-routing/index.js' +import { Providers } from '../../../src/providers.js' +import { GetProvidersHandler, type GetProvidersHandlerComponents } from '../../../src/rpc/handlers/get-providers.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import { createValues, type Value } from '../../utils/create-values.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' + +const T = MESSAGE_TYPE.GET_PROVIDERS + +describe('rpc - handlers - GetProviders', () => { + let peerId: PeerId + let sourcePeer: PeerId + let closerPeer: PeerId + let providerPeer: PeerId + let peerStore: PeerStore + let providers: SinonStubbedInstance + let peerRouting: SinonStubbedInstance + let handler: GetProvidersHandler + let values: Value[] + + beforeEach(async () => { + peerId = await createPeerId() + sourcePeer = await createPeerId() + closerPeer = await createPeerId() + providerPeer = await createPeerId() + values = await createValues(1) + + peerRouting = Sinon.createStubInstance(PeerRouting) + providers = Sinon.createStubInstance(Providers) + peerStore = new PersistentPeerStore({ + peerId, + datastore: new MemoryDatastore(), + events: new EventEmitter() + }) + + const components: GetProvidersHandlerComponents = { + peerStore + } + + handler = new GetProvidersHandler(components, { + peerRouting, + providers, + lan: false + }) + }) + + it('errors with an invalid key ', async () => { + const msg = new Message(T, uint8ArrayFromString('hello'), 0) + + await expect(handler.handle(sourcePeer, msg)).to.eventually.be.rejected().with.property('code', 'ERR_INVALID_CID') + }) + + it('responds with providers and closer peers', async () => { + const v = values[0] + const msg = new Message(T, v.cid.bytes, 0) + + const closer: PeerInfo[] = [{ + id: closerPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.2.6/tcp/4002'), + multiaddr('/ip4/21.31.57.23/tcp/4002') + ], + protocols: [] + }] + + const provider: PeerInfo[] = [{ + id: providerPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/135.4.67.0/tcp/4002') + ], + protocols: [] + }] + + providers.getProviders.withArgs(v.cid).resolves([providerPeer]) + peerRouting.getCloserPeersOffline.withArgs(msg.key, sourcePeer).resolves(closer) + + await peerStore.merge(providerPeer, { + multiaddrs: provider[0].multiaddrs + }) + await peerStore.merge(closerPeer, { + multiaddrs: closer[0].multiaddrs + }) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.key).to.be.eql(v.cid.bytes) + expect(response.providerPeers).to.have.lengthOf(1) + expect(response.providerPeers[0].id.toString()).to.equal(provider[0].id.toString()) + expect(response.closerPeers).to.have.lengthOf(1) + expect(response.closerPeers[0].id.toString()).to.equal(closer[0].id.toString()) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/get-value.spec.ts b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts new file mode 100644 index 0000000000..b8cb3b7b6b --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts @@ -0,0 +1,148 @@ +/* eslint-env mocha */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { Libp2pRecord } from '@libp2p/record' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import Sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PeerRouting } from '../../../src/peer-routing/index.js' +import { GetValueHandler, type GetValueHandlerComponents } from '../../../src/rpc/handlers/get-value.js' +import * as utils from '../../../src/utils.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Datastore } from 'interface-datastore' +import type { SinonStubbedInstance } from 'sinon' + +const T = MESSAGE_TYPE.GET_VALUE + +describe('rpc - handlers - GetValue', () => { + let peerId: PeerId + let sourcePeer: PeerId + let closerPeer: PeerId + let targetPeer: PeerId + let handler: GetValueHandler + let peerRouting: SinonStubbedInstance + let peerStore: PeerStore + let datastore: Datastore + + beforeEach(async () => { + peerId = await createPeerId() + sourcePeer = await createPeerId() + closerPeer = await createPeerId() + targetPeer = await createPeerId() + peerRouting = Sinon.createStubInstance(PeerRouting) + datastore = new MemoryDatastore() + peerStore = new PersistentPeerStore({ + peerId, + datastore, + events: new EventEmitter() + }) + + const components: GetValueHandlerComponents = { + datastore, + peerStore + } + + handler = new GetValueHandler(components, { + peerRouting + }) + }) + + it('errors when missing key', async () => { + const msg = new Message(T, new Uint8Array(0), 0) + + try { + await handler.handle(sourcePeer, msg) + } catch (err: any) { + expect(err.code).to.eql('ERR_INVALID_KEY') + return + } + + throw new Error('should error when missing key') + }) + + it('responds with a local value', async () => { + const key = uint8ArrayFromString('hello') + const value = uint8ArrayFromString('world') + const record = new Libp2pRecord(key, value, new Date()) + + await datastore.put(utils.bufferToRecordKey(key), record.serialize().subarray()) + + const msg = new Message(T, key, 0) + + peerRouting.getCloserPeersOffline.withArgs(msg.key, sourcePeer).resolves([]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.record).to.exist() + expect(response).to.have.nested.property('record.key').that.equalBytes(key) + expect(response).to.have.nested.property('record.value').that.equalBytes(value) + }) + + it('responds with closerPeers returned from the dht', async () => { + const key = uint8ArrayFromString('hello') + + peerRouting.getCloserPeersOffline.withArgs(key, sourcePeer) + .resolves([{ + id: closerPeer, + multiaddrs: [], + protocols: [] + }]) + + const msg = new Message(T, key, 0) + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response).to.have.nested.property('closerPeers[0].id').that.deep.equals(closerPeer) + }) + + describe('public key', () => { + it('peer in peerstore', async () => { + const key = utils.keyForPublicKey(targetPeer) + const msg = new Message(T, key, 0) + + if (targetPeer.publicKey == null) { + throw new Error('targetPeer had no public key') + } + + await peerStore.merge(targetPeer, { + publicKey: targetPeer.publicKey + }) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response).to.have.nested.property('record.value').that.equalBytes(targetPeer.publicKey) + }) + + it('peer not in peerstore', async () => { + const key = utils.keyForPublicKey(targetPeer) + const msg = new Message(T, key, 0) + + peerRouting.getCloserPeersOffline.resolves([]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.record).to.not.be.ok() + }) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/ping.spec.ts b/packages/kad-dht/test/rpc/handlers/ping.spec.ts new file mode 100644 index 0000000000..0c696c5441 --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/ping.spec.ts @@ -0,0 +1,31 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PingHandler } from '../../../src/rpc/handlers/ping.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { DHTMessageHandler } from '../../../src/rpc/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const T = MESSAGE_TYPE.PING + +describe('rpc - handlers - Ping', () => { + let sourcePeer: PeerId + let handler: DHTMessageHandler + + beforeEach(async () => { + sourcePeer = await createPeerId() + }) + + beforeEach(async () => { + handler = new PingHandler() + }) + + it('replies with the same message', async () => { + const msg = new Message(T, uint8ArrayFromString('hello'), 5) + const response = await handler.handle(sourcePeer, msg) + + expect(response).to.be.deep.equal(msg) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/put-value.spec.ts b/packages/kad-dht/test/rpc/handlers/put-value.spec.ts new file mode 100644 index 0000000000..720b6308bf --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/put-value.spec.ts @@ -0,0 +1,80 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +import { Libp2pRecord } from '@libp2p/record' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import delay from 'delay' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PutValueHandler } from '../../../src/rpc/handlers/put-value.js' +import * as utils from '../../../src/utils.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { Validators } from '../../../src/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +const T = MESSAGE_TYPE.PUT_VALUE + +describe('rpc - handlers - PutValue', () => { + let sourcePeer: PeerId + let handler: PutValueHandler + let datastore: Datastore + let validators: Validators + + beforeEach(async () => { + sourcePeer = await createPeerId() + datastore = new MemoryDatastore() + validators = {} + + const components = { + datastore + } + + handler = new PutValueHandler(components, { + validators + }) + }) + + it('errors on missing record', async () => { + const msg = new Message(T, uint8ArrayFromString('hello'), 5) + + try { + await handler.handle(sourcePeer, msg) + } catch (err: any) { + expect(err.code).to.eql('ERR_EMPTY_RECORD') + return + } + + throw new Error('should error on missing record') + }) + + it('stores the record in the datastore', async () => { + const msg = new Message(T, uint8ArrayFromString('/val/hello'), 5) + const record = new Libp2pRecord( + uint8ArrayFromString('hello'), + uint8ArrayFromString('world'), + new Date() + ) + msg.record = record + validators.val = async () => {} + + const response = await handler.handle(sourcePeer, msg) + expect(response).to.deep.equal(msg) + + const key = utils.bufferToRecordKey(uint8ArrayFromString('hello')) + const res = await datastore.get(key) + + const rec = Libp2pRecord.deserialize(res) + + expect(rec).to.have.property('key').eql(uint8ArrayFromString('hello')) + + if (rec.timeReceived == null) { + throw new Error('Libp2pRecord timeReceived not set') + } + + // make sure some time has passed + await delay(10) + expect(rec.timeReceived.getTime()).to.be.lessThan(Date.now()) + }) +}) diff --git a/packages/kad-dht/test/rpc/index.node.ts b/packages/kad-dht/test/rpc/index.node.ts new file mode 100644 index 0000000000..a519b3f34f --- /dev/null +++ b/packages/kad-dht/test/rpc/index.node.ts @@ -0,0 +1,114 @@ +/* eslint-env mocha */ + +import { mockStream } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { start } from '@libp2p/interfaces/startable' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import all from 'it-all' +import * as lp from 'it-length-prefixed' +import map from 'it-map' +import { pipe } from 'it-pipe' +import pDefer from 'p-defer' +import Sinon, { type SinonStubbedInstance } from 'sinon' +import { stubInterface } from 'ts-sinon' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../src/message/index.js' +import { PeerRouting } from '../../src/peer-routing/index.js' +import { Providers } from '../../src/providers.js' +import { RoutingTable } from '../../src/routing-table/index.js' +import { RPC, type RPCComponents } from '../../src/rpc/index.js' +import { createPeerId } from '../utils/create-peer-id.js' +import type { Validators } from '../../src/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { Connection } from '@libp2p/interface-connection' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Datastore } from 'interface-datastore' +import type { Duplex, Source } from 'it-stream-types' + +describe('rpc', () => { + let peerId: PeerId + let rpc: RPC + let providers: SinonStubbedInstance + let peerRouting: SinonStubbedInstance + let validators: Validators + let datastore: Datastore + let routingTable: RoutingTable + + beforeEach(async () => { + peerId = await createPeerId() + datastore = new MemoryDatastore() + + const components: RPCComponents = { + peerId, + datastore, + peerStore: stubInterface(), + addressManager: stubInterface() + } + components.peerStore = new PersistentPeerStore({ + ...components, + events: new EventEmitter() + }) + + await start(...Object.values(components)) + + providers = Sinon.createStubInstance(Providers) + peerRouting = Sinon.createStubInstance(PeerRouting) + routingTable = Sinon.createStubInstance(RoutingTable) + validators = {} + + rpc = new RPC(components, { + routingTable, + providers, + peerRouting, + validators, + lan: false + }) + }) + + it('calls back with the response', async () => { + const defer = pDefer() + const msg = new Message(MESSAGE_TYPE.GET_VALUE, uint8ArrayFromString('hello'), 5) + + const validateMessage = (res: Uint8ArrayList[]): void => { + const msg = Message.deserialize(res[0]) + expect(msg).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(msg).to.have.property('closerPeers').eql([]) + defer.resolve() + } + + peerRouting.getCloserPeersOffline.resolves([]) + + const source = pipe( + [msg.serialize()], + (source) => lp.encode(source), + source => map(source, arr => new Uint8ArrayList(arr)), + (source) => all(source) + ) + + const duplexStream: Duplex, Source, Promise> = { + source: (async function * () { + yield * source + })(), + sink: async (source) => { + const res = await pipe( + source, + (source) => lp.decode(source), + async (source) => all(source) + ) + validateMessage(res) + } + } + + rpc.onIncomingStream({ + stream: mockStream(duplexStream), + connection: stubInterface() + }) + + await defer.promise + }) +}) diff --git a/packages/kad-dht/test/utils/create-peer-id.ts b/packages/kad-dht/test/utils/create-peer-id.ts new file mode 100644 index 0000000000..5295535fb2 --- /dev/null +++ b/packages/kad-dht/test/utils/create-peer-id.ts @@ -0,0 +1,20 @@ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import type { Ed25519PeerId } from '@libp2p/interface-peer-id' + +/** + * Creates multiple PeerIds + */ +export async function createPeerIds (length: number): Promise { + return Promise.all( + new Array(length).fill(0).map(async () => createEd25519PeerId()) + ) +} + +/** + * Creates a PeerId + */ +export async function createPeerId (): Promise { + const ids = await createPeerIds(1) + + return ids[0] +} diff --git a/packages/kad-dht/test/utils/create-values.ts b/packages/kad-dht/test/utils/create-values.ts new file mode 100644 index 0000000000..b4e2e75611 --- /dev/null +++ b/packages/kad-dht/test/utils/create-values.ts @@ -0,0 +1,22 @@ +import { randomBytes } from '@libp2p/crypto' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' +import { sha256 } from 'multiformats/hashes/sha2' + +export interface Value { + cid: CID + value: Uint8Array +} + +export async function createValues (length: number): Promise { + return Promise.all( + Array.from({ length }).map(async () => { + const bytes = randomBytes(32) + const h = await sha256.digest(bytes) + return { + cid: CID.createV1(raw.code, h), + value: bytes + } + }) + ) +} diff --git a/packages/kad-dht/test/utils/index.ts b/packages/kad-dht/test/utils/index.ts new file mode 100644 index 0000000000..933d9e7c8b --- /dev/null +++ b/packages/kad-dht/test/utils/index.ts @@ -0,0 +1,11 @@ +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * Count how many peers are in b but are not in a + */ +export function countDiffPeers (a: PeerId[], b: PeerId[]): number { + const s = new Set() + a.forEach((p) => s.add(p.toString())) + + return b.filter((p) => !s.has(p.toString())).length +} diff --git a/packages/kad-dht/test/utils/sort-closest-peers.ts b/packages/kad-dht/test/utils/sort-closest-peers.ts new file mode 100644 index 0000000000..1fb81e5322 --- /dev/null +++ b/packages/kad-dht/test/utils/sort-closest-peers.ts @@ -0,0 +1,28 @@ +import all from 'it-all' +import map from 'it-map' +import { compare as uint8ArrayCompare } from 'uint8arrays/compare' +import { xor as uint8ArrayXor } from 'uint8arrays/xor' +import { convertPeerId } from '../../src/utils.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * Sort peers by distance to the given `kadId` + */ +export async function sortClosestPeers (peers: PeerId[], kadId: Uint8Array): Promise { + const distances = await all( + map(peers, async (peer) => { + const id = await convertPeerId(peer) + + return { + peer, + distance: uint8ArrayXor(id, kadId) + } + }) + ) + + return distances + .sort((a, b) => { + return uint8ArrayCompare(a.distance, b.distance) + }) + .map((d) => d.peer) +} diff --git a/packages/kad-dht/test/utils/test-dht.ts b/packages/kad-dht/test/utils/test-dht.ts new file mode 100644 index 0000000000..e490b944d3 --- /dev/null +++ b/packages/kad-dht/test/utils/test-dht.ts @@ -0,0 +1,178 @@ +import { mockRegistrar, mockConnectionManager, mockNetwork } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { start, stop } from '@libp2p/interfaces/startable' +import { logger } from '@libp2p/logger' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { multiaddr } from '@multiformats/multiaddr' +import { MemoryDatastore } from 'datastore-core/memory' +import delay from 'delay' +import pRetry from 'p-retry' +import { stubInterface } from 'ts-sinon' +import { DefaultDualKadDHT } from '../../src/dual-kad-dht.js' +import { createPeerId } from './create-peer-id.js' +import type { DualKadDHT, KadDHTComponents, KadDHTInit } from '../../src/index.js' +import type { DefaultKadDHT } from '../../src/kad-dht.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Registrar } from '@libp2p/interface-registrar' + +const log = logger('libp2p:kad-dht:test-dht') + +export class TestDHT { + private readonly peers: Map + + constructor () { + this.peers = new Map() + } + + async spawn (options: Partial = {}, autoStart = true): Promise { + const events = new EventEmitter() + const components: KadDHTComponents = { + peerId: await createPeerId(), + datastore: new MemoryDatastore(), + registrar: mockRegistrar(), + // connectionGater: mockConnectionGater(), + addressManager: stubInterface(), + peerStore: stubInterface(), + connectionManager: stubInterface(), + events + } + components.connectionManager = mockConnectionManager({ + ...components, + events + }) + components.peerStore = new PersistentPeerStore({ + ...components, + events + }) + + await start(...Object.values(components)) + + mockNetwork.addNode({ + ...components, + events + }) + + const addressManager = stubInterface() + addressManager.getAddresses.returns([ + multiaddr(`/ip4/127.0.0.1/tcp/4002/p2p/${components.peerId.toString()}`), + multiaddr(`/ip4/192.168.1.1/tcp/4002/p2p/${components.peerId.toString()}`), + multiaddr(`/ip4/85.3.31.0/tcp/4002/p2p/${components.peerId.toString()}`) + ]) + + components.addressManager = addressManager + + const opts: KadDHTInit = { + validators: { + async v () { + + }, + async v2 () { + + } + }, + selectors: { + v: () => 0 + }, + querySelfInterval: 600000, + initialQuerySelfInterval: 600000, + allowQueryWithZeroPeers: true, + ...options + } + + const dht = new DefaultDualKadDHT(components, opts) + + // simulate libp2p._onDiscoveryPeer + dht.addEventListener('peer', (evt) => { + const peerData = evt.detail + + if (components.peerId.equals(peerData.id)) { + return + } + + components.peerStore.merge(peerData.id, { + multiaddrs: peerData.multiaddrs, + protocols: peerData.protocols + }) + .catch(err => { log.error(err) }) + }) + + if (autoStart) { + await dht.start() + } + + this.peers.set(components.peerId.toString(), { + dht, + registrar: components.registrar + }) + + return dht + } + + async connect (dhtA: DefaultDualKadDHT, dhtB: DefaultDualKadDHT): Promise { + // need addresses in the address book otherwise we won't know whether to add + // the peer to the public or private DHT and will do nothing + await dhtA.components.peerStore.merge(dhtB.components.peerId, { + multiaddrs: dhtB.components.addressManager.getAddresses() + }) + await dhtB.components.peerStore.merge(dhtA.components.peerId, { + multiaddrs: dhtA.components.addressManager.getAddresses() + }) + + await dhtA.components.connectionManager.openConnection(dhtB.components.peerId) + + // wait for peers to appear in each others' routing tables + await checkConnected(dhtA.lan, dhtB.lan) + + // only wait for WANs to connect if we are in server mode + if ((await dhtA.wan.getMode()) === 'server' && (await dhtB.wan.getMode()) === 'server') { + await checkConnected(dhtA.wan, dhtB.wan) + } + + async function checkConnected (a: DefaultKadDHT, b: DefaultKadDHT): Promise { + const routingTableChecks = [] + + routingTableChecks.push(async () => { + const match = await a.routingTable.find(dhtB.components.peerId) + + if (match == null) { + await delay(100) + throw new Error('not found') + } + + return match + }) + + routingTableChecks.push(async () => { + const match = await b.routingTable.find(dhtA.components.peerId) + + if (match == null) { + await delay(100) + throw new Error('not found') + } + + return match + }) + + // Check routing tables + return Promise.all( + routingTableChecks + .map( + async check => pRetry(check, { retries: 50 }) + ) + ) + } + } + + async teardown (): Promise { + await Promise.all( + Array.from(this.peers.entries()).map(async ([_, { dht }]) => { + await stop(dht) + }) + ) + this.peers.clear() + } +} diff --git a/packages/kad-dht/tsconfig.json b/packages/kad-dht/tsconfig.json new file mode 100644 index 0000000000..b0b4b2243b --- /dev/null +++ b/packages/kad-dht/tsconfig.json @@ -0,0 +1,78 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-address-manager" + }, + { + "path": "../interface-connection" + }, + { + "path": "../interface-connection-manager" + }, + { + "path": "../interface-content-routing" + }, + { + "path": "../interface-libp2p" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-peer-discovery" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-info" + }, + { + "path": "../interface-peer-routing" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interface-registrar" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../peer-store" + }, + { + "path": "../record" + }, + { + "path": "../topology" + } + ] +} diff --git a/packages/keychain/CHANGELOG.md b/packages/keychain/CHANGELOG.md new file mode 100644 index 0000000000..0f7c302ac5 --- /dev/null +++ b/packages/keychain/CHANGELOG.md @@ -0,0 +1,204 @@ +## [2.0.1](https://github.com/libp2p/js-libp2p-keychain/compare/v2.0.0...v2.0.1) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7fd8023](https://github.com/libp2p/js-libp2p-keychain/commit/7fd80233db0b8706eb0ffe5372c6bad584ec211f)) +* Update .github/workflows/stale.yml [skip ci] ([c185b0d](https://github.com/libp2p/js-libp2p-keychain/commit/c185b0de456611ca42ec49bc7d52f803e4a76930)) + + +### Dependencies + +* **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)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-keychain/compare/v1.0.1...v2.0.0) (2023-03-13) + + +### ⚠ BREAKING CHANGES + +* requires most recent datastore implementation + +### Bug Fixes + +* update datastore dependency ([#58](https://github.com/libp2p/js-libp2p-keychain/issues/58)) ([a8a1628](https://github.com/libp2p/js-libp2p-keychain/commit/a8a162875e48f23611190c3fb31e439da1d2d64b)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-keychain/compare/v1.0.0...v1.0.1) (2023-03-13) + + +### Bug Fixes + +* replace err-code with CodeError ([#57](https://github.com/libp2p/js-libp2p-keychain/issues/57)) ([cc752d9](https://github.com/libp2p/js-libp2p-keychain/commit/cc752d9349a622f013cb3b713d09a663b1169766)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([f3985cc](https://github.com/libp2p/js-libp2p-keychain/commit/f3985cc47ae966a33537af3f58c071f6c58184c9)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([d8b81ff](https://github.com/libp2p/js-libp2p-keychain/commit/d8b81ff5e03ca56541ae2117a928dedf180e85ac)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([a0a6972](https://github.com/libp2p/js-libp2p-keychain/commit/a0a6972d7af40488344e619e116f4d665190db6e)) +* Update .github/workflows/stale.yml [skip ci] ([b2cf129](https://github.com/libp2p/js-libp2p-keychain/commit/b2cf129fb1a3e0263a03d5a8a0e1ee74cd543004)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.6.1...v1.0.0) (2023-01-27) + + +### ⚠ BREAKING CHANGES + +* this module is now typescript and does not store the self key on startup. cms operations have also been moved to [@libp2p/cms](https://www.npmjs.com/@libp2p/cms) + +### Features + +* convert to typescript ([#53](https://github.com/libp2p/js-libp2p-keychain/issues/53)) ([3544df7](https://github.com/libp2p/js-libp2p-keychain/commit/3544df7c119b8cebded3f5c483e9f44bf499280f)) + + +### Trivial Changes + +* add deprecation notice ([#50](https://github.com/libp2p/js-libp2p-keychain/issues/50)) ([2a9b99c](https://github.com/libp2p/js-libp2p-keychain/commit/2a9b99cd402ed7260ebcac49d9e44905697beee0)) + + +## [0.6.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.6.0...v0.6.1) (2020-06-09) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.4...v0.6.0) (2019-12-18) + + + + +## [0.5.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.3...v0.5.4) (2019-12-18) + + + + +## [0.5.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.2...v0.5.3) (2019-12-18) + + + + +## [0.5.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.1...v0.5.2) (2019-12-02) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.0...v0.5.1) (2019-09-25) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.2...v0.5.0) (2019-08-16) + + +* refactor: use async/await instead of callbacks (#37) ([dda315a](https://github.com/libp2p/js-libp2p-keychain/commit/dda315a)), closes [#37](https://github.com/libp2p/js-libp2p-keychain/issues/37) + + +### BREAKING CHANGES + +* The api now uses async/await instead of callbacks. + +Co-Authored-By: Vasco Santos + + + + +## [0.4.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.1...v0.4.2) (2019-06-13) + + +### Bug Fixes + +* throw errors with correct stack trace ([#35](https://github.com/libp2p/js-libp2p-keychain/issues/35)) ([7051b9c](https://github.com/libp2p/js-libp2p-keychain/commit/7051b9c)) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.0...v0.4.1) (2019-03-14) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.6...v0.4.0) (2019-02-26) + + +### Features + +* adds support for ed25199 and secp256k1 ([#31](https://github.com/libp2p/js-libp2p-keychain/issues/31)) ([9eb11f4](https://github.com/libp2p/js-libp2p-keychain/commit/9eb11f4)) + + + + +## [0.3.6](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.5...v0.3.6) (2019-01-10) + + +### Bug Fixes + +* reduce bundle size ([#28](https://github.com/libp2p/js-libp2p-keychain/issues/28)) ([7eeed87](https://github.com/libp2p/js-libp2p-keychain/commit/7eeed87)) + + + + +## [0.3.5](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.4...v0.3.5) (2019-01-10) + + + + +## [0.3.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.3...v0.3.4) (2019-01-04) + + + + +## [0.3.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.2...v0.3.3) (2018-10-25) + + + + +## [0.3.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.1...v0.3.2) (2018-09-18) + + +### Bug Fixes + +* validate createKey params properly ([#26](https://github.com/libp2p/js-libp2p-keychain/issues/26)) ([8dfaab1](https://github.com/libp2p/js-libp2p-keychain/commit/8dfaab1)) + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.0...v0.3.1) (2018-01-29) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.1...v0.3.0) (2018-01-29) + + +### Bug Fixes + +* deepmerge 2.0.1 fails in browser, stay with 1.5.2 ([2ce4444](https://github.com/libp2p/js-libp2p-keychain/commit/2ce4444)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.0...v0.2.1) (2017-12-28) + + +### Features + +* generate unique options for a key chain ([#20](https://github.com/libp2p/js-libp2p-keychain/issues/20)) ([89a451c](https://github.com/libp2p/js-libp2p-keychain/commit/89a451c)) + + + + +# 0.2.0 (2017-12-20) + + +### Bug Fixes + +* error message ([8305d20](https://github.com/libp2p/js-libp2p-keychain/commit/8305d20)) +* lint errors ([06917f7](https://github.com/libp2p/js-libp2p-keychain/commit/06917f7)) +* lint errors ([ff4f656](https://github.com/libp2p/js-libp2p-keychain/commit/ff4f656)) +* linting ([409a999](https://github.com/libp2p/js-libp2p-keychain/commit/409a999)) +* maps an IPFS hash name to its forge equivalent ([f71d3a6](https://github.com/libp2p/js-libp2p-keychain/commit/f71d3a6)), closes [#12](https://github.com/libp2p/js-libp2p-keychain/issues/12) +* more linting ([7c44c91](https://github.com/libp2p/js-libp2p-keychain/commit/7c44c91)) +* return info on removed key [#10](https://github.com/libp2p/js-libp2p-keychain/issues/10) ([f49e753](https://github.com/libp2p/js-libp2p-keychain/commit/f49e753)) + + +### 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)) diff --git a/packages/keychain/LICENSE b/packages/keychain/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/keychain/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/keychain/LICENSE-APACHE b/packages/keychain/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/keychain/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/keychain/LICENSE-MIT b/packages/keychain/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/keychain/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/keychain/README.md b/packages/keychain/README.md new file mode 100644 index 0000000000..1998e1b7a1 --- /dev/null +++ b/packages/keychain/README.md @@ -0,0 +1,96 @@ +# @libp2p/keychain + +[![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) + +> Key management and cryptographically protected messages + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Features + +- Manages the lifecycle of a key +- Keys are encrypted at rest +- Enforces the use of safe key names +- Uses encrypted PKCS 8 for key storage +- Uses PBKDF2 for a "stetched" key encryption key +- Enforces NIST SP 800-131A and NIST SP 800-132 +- Delays reporting errors to slow down brute force attacks + +### KeyInfo + +The key management and naming service API all return a `KeyInfo` object. The `id` is a universally unique identifier for the key. The `name` is local to the key chain. + +```js +{ + name: 'rsa-key', + id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW' +} +``` + +The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multihash) of its public key. The *public key* is a [protobuf encoding](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/keys.proto.js) containing a type and the [DER encoding](https://en.wikipedia.org/wiki/X.690) of the PKCS [SubjectPublicKeyInfo](https://www.ietf.org/rfc/rfc3279.txt). + +### Private key storage + +A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. + +The default options for generating the derived encryption key are in the `dek` object. This, along with the passPhrase, is the input to a `PBKDF2` function. + +```js +const defaultOptions = { + //See https://cryptosense.com/parameter-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 1000, + salt: 'at least 16 characters long', + hash: 'sha2-512' + } +} +``` + +![key storage](./doc/private-key.png?raw=true) + +### Physical storage + +The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benefit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/keychain/doc/private-key.png b/packages/keychain/doc/private-key.png new file mode 100644 index 0000000000..4c85dc610c Binary files /dev/null and b/packages/keychain/doc/private-key.png differ diff --git a/packages/keychain/doc/private-key.xml b/packages/keychain/doc/private-key.xml new file mode 100644 index 0000000000..51cb8c5a9b --- /dev/null +++ b/packages/keychain/doc/private-key.xml @@ -0,0 +1 @@ +7VlNb6MwEP01HLfCGBJ6bNJ2V9pdqVIP2x4dcMAKYGScJumvXxNsvkw+SmgSVe2hMs9mbL839swQA07j9U+G0vAv9XFkWKa/NuC9YVmua4n/ObApAOjCAggY8QsIVMAzeccSNCW6JD7OGgM5pREnaRP0aJJgjzcwxBhdNYfNadScNUUB1oBnD0U6+o/4PJTbssYV/guTIFQzg9Ft0TND3iJgdJnI+QwLzrd/RXeMlC250SxEPl3VIPhgwCmjlBeteD3FUU6toq1473FHb7luhhN+zAtSpzcULeXWU5RluYmQoQzLRfKNIobjtbA7CXkcCQCIZsYZXeApjSgTSEITMXIyJ1HUglBEgkQ8emJlWOCTN8w4EZTfyY6Y+H4+zWQVEo6fU+Tlc66EfwlsSynOF22KJ7loYQCvd24clHQKL8U0xpxtxBDlolIA6aBgJJ9Xldy2hMKa0ko3JB0sKA1XJIuG5Lmbc6hx/jT5ff9oaWQL50jzZsqoh4Uq3dTUtBiAF9AmxtaJAVYHM6MBmLE1Zny8EABNOaFJ9nW9sfQryfr4fN7oaJxrNOPEv8sv1ZyvSFwPxGuSLjbJNi85GzcmGCvgdQvAUQk8YUbE8nK6a7xhX7uKD7JWo8XpoEVhDEeIk7em+S6u5AxPlIiJq6PQEgWMraaJjC6Zh+Vb9Uu2bUiFw12GOGIB5pqhrXTlto9SczSomk5Dyw9IJsL1dku1C+9SKpYHR5Fvmj1VhE1D2ukbTkX3WlQsuGmErbqw4KLnE5oHBDlWWbt10K22i+xQVgiANrVhaT4g271g22xfKI3kTDQKi33d5rY7fB4Mmgxn5B3NtgNy/5D7EKOdieHcfyhcRmiGo0mZBauwW+XBe+KlzOblSoxSz7pjunvj6A8RgcpaY9Mw3tfZ1BA6n2f41IOt6puaRAucrz/AiSbUNaR/Fjxj+geAxk668PJqRLiPexX8QPuS/OjVmo84yjhleqV2CXac9o18Vnb06uEm3e01PvWW8XZfh4iZFdn+n9mQTLWSCQhcjanRntB5ElF6yl9cQl++zGpfbo7unp9VZgE9M2dJoFFdbRmc5cRarRMLLd0P3S5KnAEoGWuUaHwcTHPXhL/U2q/NjPdF+k6tIHV6J8AqeF9PBtzyZxu2HLVvaQPdlqHhShswaG0zmLQdVWsRbb+lPV5avf44Qdpm2Vo/67JLnfb+oo86RDeNKxLdHkr0208TXcXGz/pW0S066C+61SG6/S36x0TXC7VTRP9SH43VLahyzHZpc/xHY7DfUG85xWP1A2MxvPoRFz78Bw== \ No newline at end of file diff --git a/packages/keychain/package.json b/packages/keychain/package.json new file mode 100644 index 0000000000..9bc7e246b0 --- /dev/null +++ b/packages/keychain/package.json @@ -0,0 +1,76 @@ +{ + "name": "@libp2p/keychain", + "version": "2.0.1", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "crypto", + "encryption", + "keys", + "libp2p", + "secure" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-keychain": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "interface-datastore": "^8.2.0", + "merge-options": "^3.0.4", + "sanitize-filename": "^1.6.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@libp2p/peer-id-factory": "^2.0.0", + "aegir": "^39.0.10", + "datastore-core": "^9.0.1", + "multiformats": "^11.0.1" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/keychain/src/errors.ts b/packages/keychain/src/errors.ts new file mode 100644 index 0000000000..791d87fd3b --- /dev/null +++ b/packages/keychain/src/errors.ts @@ -0,0 +1,18 @@ + +export enum codes { + ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', + ERR_INVALID_KEY_NAME = 'ERR_INVALID_KEY_NAME', + ERR_INVALID_KEY_TYPE = 'ERR_INVALID_KEY_TYPE', + ERR_KEY_ALREADY_EXISTS = 'ERR_KEY_ALREADY_EXISTS', + ERR_INVALID_KEY_SIZE = 'ERR_INVALID_KEY_SIZE', + ERR_KEY_NOT_FOUND = 'ERR_KEY_NOT_FOUND', + ERR_OLD_KEY_NAME_INVALID = 'ERR_OLD_KEY_NAME_INVALID', + ERR_NEW_KEY_NAME_INVALID = 'ERR_NEW_KEY_NAME_INVALID', + ERR_PASSWORD_REQUIRED = 'ERR_PASSWORD_REQUIRED', + ERR_PEM_REQUIRED = 'ERR_PEM_REQUIRED', + ERR_CANNOT_READ_KEY = 'ERR_CANNOT_READ_KEY', + ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY', + ERR_INVALID_OLD_PASS_TYPE = 'ERR_INVALID_OLD_PASS_TYPE', + ERR_INVALID_NEW_PASS_TYPE = 'ERR_INVALID_NEW_PASS_TYPE', + ERR_INVALID_PASS_LENGTH = 'ERR_INVALID_PASS_LENGTH' +} diff --git a/packages/keychain/src/index.ts b/packages/keychain/src/index.ts new file mode 100644 index 0000000000..5c0a94ee3f --- /dev/null +++ b/packages/keychain/src/index.ts @@ -0,0 +1,579 @@ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { pbkdf2, randomBytes } from '@libp2p/crypto' +import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { Key } from 'interface-datastore/key' +import mergeOptions from 'merge-options' +import sanitize from 'sanitize-filename' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { codes } from './errors.js' +import type { KeyChain, KeyInfo, KeyType } from '@libp2p/interface-keychain' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +const log = logger('libp2p:keychain') + +export interface DEKConfig { + hash: string + salt: string + iterationCount: number + keyLength: number +} + +export interface KeyChainInit { + pass?: string + dek?: DEKConfig +} + +const keyPrefix = '/pkcs8/' +const infoPrefix = '/info/' +const privates = new WeakMap() + +// NIST SP 800-132 +const NIST = { + minKeyLength: 112 / 8, + minSaltLength: 128 / 8, + minIterationCount: 1000 +} + +const defaultOptions = { + // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + salt: 'you should override this value with a crypto secure random number', + hash: 'sha2-512' + } +} + +function validateKeyName (name: string): boolean { + if (name == null) { + return false + } + if (typeof name !== 'string') { + return false + } + return name === sanitize(name.trim()) && name.length > 0 +} + +/** + * Throws an error after a delay + * + * This assumes than an error indicates that the keychain is under attack. Delay returning an + * error to make brute force attacks harder. + */ +async function randomDelay (): Promise { + const min = 200 + const max = 1000 + const delay = Math.random() * (max - min) + min + + await new Promise(resolve => setTimeout(resolve, delay)) +} + +/** + * Converts a key name into a datastore name + */ +function DsName (name: string): Key { + return new Key(keyPrefix + name) +} + +/** + * Converts a key name into a datastore info name + */ +function DsInfoName (name: string): Key { + return new Key(infoPrefix + name) +} + +export interface KeyChainComponents { + datastore: Datastore +} + +/** + * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. + * + * A key in the store has two entries + * - '/info/*key-name*', contains the KeyInfo for the key + * - '/pkcs8/*key-name*', contains the PKCS #8 for the key + * + */ +export class DefaultKeyChain implements KeyChain { + private readonly components: KeyChainComponents + private readonly init: KeyChainInit + + /** + * Creates a new instance of a key chain + */ + constructor (components: KeyChainComponents, init: KeyChainInit) { + this.components = components + this.init = mergeOptions(defaultOptions, init) + + // Enforce NIST SP 800-132 + if (this.init.pass != null && this.init.pass?.length < 20) { + throw new Error('pass must be least 20 characters') + } + if (this.init.dek?.keyLength != null && this.init.dek.keyLength < NIST.minKeyLength) { + throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) + } + if (this.init.dek?.salt?.length != null && this.init.dek.salt.length < NIST.minSaltLength) { + throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) + } + if (this.init.dek?.iterationCount != null && this.init.dek.iterationCount < NIST.minIterationCount) { + throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) + } + + const dek = this.init.pass != null && this.init.dek?.salt != null + ? pbkdf2( + this.init.pass, + this.init.dek?.salt, + this.init.dek?.iterationCount, + this.init.dek?.keyLength, + this.init.dek?.hash) + : '' + + privates.set(this, { dek }) + } + + /** + * Generates the options for a keychain. A random salt is produced. + * + * @returns {object} + */ + static generateOptions (): KeyChainInit { + const options = Object.assign({}, defaultOptions) + const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding + options.dek.salt = uint8ArrayToString(randomBytes(saltLength), 'base64') + return options + } + + /** + * Gets an object that can encrypt/decrypt protected data. + * The default options for a keychain. + * + * @returns {object} + */ + static get options (): typeof defaultOptions { + return defaultOptions + } + + /** + * Create a new key. + * + * @param {string} name - The local key name; cannot already exist. + * @param {string} type - One of the key types; 'rsa'. + * @param {number} [size = 2048] - The key size in bits. Used for rsa keys only + */ + async createKey (name: string, type: KeyType, size = 2048): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) + } + + if (typeof type !== 'string') { + await randomDelay() + throw new CodeError('Invalid key type', codes.ERR_INVALID_KEY_TYPE) + } + + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) + } + + switch (type.toLowerCase()) { + case 'rsa': + if (!Number.isSafeInteger(size) || size < 2048) { + await randomDelay() + throw new CodeError('Invalid RSA key size', codes.ERR_INVALID_KEY_SIZE) + } + break + default: + break + } + + let keyInfo + try { + const keypair = await generateKeyPair(type, size) + const kid = await keypair.id() + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await keypair.export(dek) + keyInfo = { + name, + id: kid + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + + await batch.commit() + } catch (err: any) { + await randomDelay() + throw err + } + + return keyInfo + } + + /** + * List all the keys. + * + * @returns {Promise} + */ + async listKeys (): Promise { + const query = { + prefix: infoPrefix + } + + const info = [] + for await (const value of this.components.datastore.query(query)) { + info.push(JSON.parse(uint8ArrayToString(value.value))) + } + + return info + } + + /** + * Find a key by it's id + */ + async findKeyById (id: string): Promise { + try { + const keys = await this.listKeys() + const key = keys.find((k) => k.id === id) + + if (key == null) { + throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + + return key + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Find a key by it's name. + * + * @param {string} name - The local key name. + * @returns {Promise} + */ + async findKeyByName (name: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + + const dsname = DsInfoName(name) + try { + const res = await this.components.datastore.get(dsname) + return JSON.parse(uint8ArrayToString(res)) + } catch (err: any) { + await randomDelay() + log.error(err) + throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + } + + /** + * Remove an existing key. + * + * @param {string} name - The local key name; must already exist. + * @returns {Promise} + */ + async removeKey (name: string): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + const dsname = DsName(name) + const keyInfo = await this.findKeyByName(name) + const batch = this.components.datastore.batch() + batch.delete(dsname) + batch.delete(DsInfoName(name)) + await batch.commit() + return keyInfo + } + + /** + * Rename a key + * + * @param {string} oldName - The old local key name; must already exist. + * @param {string} newName - The new local key name; must not already exist. + * @returns {Promise} + */ + async renameKey (oldName: string, newName: string): Promise { + if (!validateKeyName(oldName) || oldName === 'self') { + await randomDelay() + throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) + } + if (!validateKeyName(newName) || newName === 'self') { + await randomDelay() + throw new CodeError(`Invalid new key name '${newName}'`, codes.ERR_NEW_KEY_NAME_INVALID) + } + const oldDsname = DsName(oldName) + const newDsname = DsName(newName) + const oldInfoName = DsInfoName(oldName) + const newInfoName = DsInfoName(newName) + + const exists = await this.components.datastore.has(newDsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${newName}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + try { + const pem = await this.components.datastore.get(oldDsname) + const res = await this.components.datastore.get(oldInfoName) + + const keyInfo = JSON.parse(uint8ArrayToString(res)) + keyInfo.name = newName + const batch = this.components.datastore.batch() + batch.put(newDsname, pem) + batch.put(newInfoName, uint8ArrayFromString(JSON.stringify(keyInfo))) + batch.delete(oldDsname) + batch.delete(oldInfoName) + await batch.commit() + return keyInfo + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PEM encrypted PKCS #8 string + */ + async exportKey (name: string, password: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (password == null) { + await randomDelay() + throw new CodeError('Password is required', codes.ERR_PASSWORD_REQUIRED) + } + + const dsname = DsName(name) + try { + const res = await this.components.datastore.get(dsname) + const pem = uint8ArrayToString(res) + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const privateKey = await importKey(pem, dek) + const keyString = await privateKey.export(password) + + return keyString + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PeerId + */ + async exportPeerId (name: string): Promise { + const password = 'temporary-password' + const pem = await this.exportKey(name, password) + const privateKey = await importKey(pem, password) + + return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) + } + + /** + * Import a new key from a PEM encoded PKCS #8 string + * + * @param {string} name - The local key name; must not already exist. + * @param {string} pem - The PEM encoded PKCS #8 string + * @param {string} password - The password. + * @returns {Promise} + */ + async importKey (name: string, pem: string, password: string): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (pem == null) { + await randomDelay() + throw new CodeError('PEM encoded key is required', codes.ERR_PEM_REQUIRED) + } + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + let privateKey + try { + privateKey = await importKey(pem, password) + } catch (err: any) { + await randomDelay() + throw new CodeError('Cannot read the key, most likely the password is wrong', codes.ERR_CANNOT_READ_KEY) + } + + let kid + try { + kid = await privateKey.id() + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + pem = await privateKey.export(dek) + } catch (err: any) { + await randomDelay() + throw err + } + + const keyInfo = { + name, + id: kid + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + + return keyInfo + } + + /** + * Import a peer key + */ + async importPeer (name: string, peer: PeerId): Promise { + try { + if (!validateKeyName(name)) { + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (peer == null) { + throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) + } + if (peer.privateKey == null) { + throw new CodeError('PeerId.privKey is required', codes.ERR_MISSING_PRIVATE_KEY) + } + + const privateKey = await unmarshalPrivateKey(peer.privateKey) + + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await privateKey.export(dek) + const keyInfo: KeyInfo = { + name, + id: peer.toString() + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + return keyInfo + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Gets the private key as PEM encoded PKCS #8 string + */ + async getPrivateKey (name: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + + try { + const dsname = DsName(name) + const res = await this.components.datastore.get(dsname) + return uint8ArrayToString(res) + } catch (err: any) { + await randomDelay() + log.error(err) + throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + } + + /** + * Rotate keychain password and re-encrypt all associated keys + */ + async rotateKeychainPass (oldPass: string, newPass: string): Promise { + if (typeof oldPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) + } + if (typeof newPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) + } + if (newPass.length < 20) { + await randomDelay() + throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) + } + log('recreating keychain') + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const oldDek = cached.dek + this.init.pass = newPass + const newDek = newPass != null && this.init.dek?.salt != null + ? pbkdf2( + newPass, + this.init.dek.salt, + this.init.dek?.iterationCount, + this.init.dek?.keyLength, + this.init.dek?.hash) + : '' + privates.set(this, { dek: newDek }) + const keys = await this.listKeys() + for (const key of keys) { + const res = await this.components.datastore.get(DsName(key.name)) + const pem = uint8ArrayToString(res) + const privateKey = await importKey(pem, oldDek) + const password = newDek.toString() + const keyAsPEM = await privateKey.export(password) + + // Update stored key + const batch = this.components.datastore.batch() + const keyInfo = { + name: key.name, + id: key.id + } + batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM)) + batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + } + log('keychain reconstructed') + } +} diff --git a/packages/keychain/src/util.ts b/packages/keychain/src/util.ts new file mode 100644 index 0000000000..e708fd1e03 --- /dev/null +++ b/packages/keychain/src/util.ts @@ -0,0 +1,16 @@ +/** + * Finds the first item in a collection that is matched in the + * `asyncCompare` function. + * + * `asyncCompare` is an async function that must + * resolve to either `true` or `false`. + * + * @param {Array} array + * @param {function(*)} asyncCompare - An async function that returns a boolean + */ +export async function findAsync (array: T[], asyncCompare: (val: T) => Promise): Promise { + const promises = array.map(asyncCompare) + const results = await Promise.all(promises) + const index = results.findIndex(result => result) + return array[index] +} diff --git a/packages/keychain/test/keychain.spec.ts b/packages/keychain/test/keychain.spec.ts new file mode 100644 index 0000000000..6089b90734 --- /dev/null +++ b/packages/keychain/test/keychain.spec.ts @@ -0,0 +1,552 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { pbkdf2 } from '@libp2p/crypto' +import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { createFromPrivKey } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import { Key } from 'interface-datastore/key' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { DefaultKeyChain, type KeyChainInit } from '../src/index.js' +import type { KeyChain, KeyInfo } from '@libp2p/interface-keychain' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +describe('keychain', () => { + const passPhrase = 'this is not a secure phrase' + const rsaKeyName = 'tajné jméno' + const renamedRsaKeyName = 'ชื่อลับ' + let rsaKeyInfo: KeyInfo + let ks: DefaultKeyChain + let datastore2: Datastore + + before(async () => { + datastore2 = new MemoryDatastore() + + ks = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: passPhrase }) + }) + + it('can start without a password', async () => { + await expect(async function () { + return new DefaultKeyChain({ + datastore: datastore2 + }, {}) + }()).to.eventually.be.ok() + }) + + it('needs a NIST SP 800-132 non-weak pass phrase', async () => { + await expect(async function () { + return new DefaultKeyChain({ + datastore: datastore2 + }, { pass: '< 20 character' }) + }()).to.eventually.be.rejected() + }) + + it('has default options', () => { + expect(DefaultKeyChain.options).to.exist() + }) + + it('supports supported hashing alorithms', async () => { + const ok = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) + expect(ok).to.exist() + }) + + it('does not support unsupported hashing alorithms', async () => { + await expect(async function () { + return new DefaultKeyChain({ + datastore: datastore2 + }, { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) + }()).to.eventually.be.rejected() + }) + + it('can list keys without a password', async () => { + const keychain = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + + expect(await keychain.listKeys()).to.have.lengthOf(0) + }) + + it('can find a key without a password', async () => { + const keychain = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + const keychainWithPassword = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: `hello-${Date.now()}-${Date.now()}` }) + const name = `key-${Math.random()}` + + const { id } = await keychainWithPassword.createKey(name, 'Ed25519') + + await expect(keychain.findKeyById(id)).to.eventually.be.ok() + }) + + it('can remove a key without a password', async () => { + const keychainWithoutPassword = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + const keychainWithPassword = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: `hello-${Date.now()}-${Date.now()}` }) + const name = `key-${Math.random()}` + + expect(await keychainWithPassword.createKey(name, 'Ed25519')).to.have.property('name', name) + expect(await keychainWithoutPassword.findKeyByName(name)).to.have.property('name', name) + await keychainWithoutPassword.removeKey(name) + await expect(keychainWithoutPassword.findKeyByName(name)).to.be.rejectedWith(/does not exist/) + }) + + it('requires a name to create a password', async () => { + const keychain = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + + // @ts-expect-error invalid parameters + await expect(keychain.createKey(undefined, 'derp')).to.eventually.be.rejected() + }) + + it('can generate options', async () => { + const options = DefaultKeyChain.generateOptions() + options.pass = passPhrase + const chain = new DefaultKeyChain({ + datastore: datastore2 + }, options) + expect(chain).to.exist() + }) + + describe('key name', () => { + it('is a valid filename and non-ASCII', async () => { + const errors = await Promise.all([ + ks.removeKey('../../nasty').catch(err => err), + ks.removeKey('').catch(err => err), + ks.removeKey(' ').catch(err => err), + // @ts-expect-error invalid parameters + ks.removeKey(null).catch(err => err), + // @ts-expect-error invalid parameters + ks.removeKey(undefined).catch(err => err) + ]) + + expect(errors).to.have.length(5) + errors.forEach(error => { + expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + }) + + describe('key', () => { + it('can be an RSA key', async () => { + rsaKeyInfo = await ks.createKey(rsaKeyName, 'RSA', 2048) + expect(rsaKeyInfo).to.exist() + expect(rsaKeyInfo).to.have.property('name', rsaKeyName) + expect(rsaKeyInfo).to.have.property('id') + }) + + it('is encrypted PEM encoded PKCS #8', async () => { + const pem = await ks.getPrivateKey(rsaKeyName) + return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) + + it('throws if an invalid private key name is given', async () => { + // @ts-expect-error invalid parameters + await expect(ks.getPrivateKey(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('throws if a private key cant be found', async () => { + await expect(ks.getPrivateKey('not real')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('does not overwrite existing key', async () => { + await expect(ks.createKey(rsaKeyName, 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot create the "self" key', async () => { + await expect(ks.createKey('self', 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('should validate name is string', async () => { + // @ts-expect-error invalid parameters + await expect(ks.createKey(5, 'rsa', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('should validate type is string', async () => { + // @ts-expect-error invalid parameters + await expect(ks.createKey(`TEST-${Date.now()}`, null, 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_TYPE') + }) + + it('should validate size is integer', async () => { + // @ts-expect-error invalid parameters + await expect(ks.createKey(`TEST-${Date.now()}`, 'RSA', 'string')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') + }) + + describe('implements NIST SP 800-131A', () => { + it('disallows RSA length < 2048', async () => { + await expect(ks.createKey('bad-nist-rsa', 'RSA', 1024)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') + }) + }) + }) + + describe('Ed25519 keys', () => { + const keyName = 'my custom key' + it('can be an Ed25519 key', async () => { + const keyInfo = await ks.createKey(keyName, 'Ed25519') + expect(keyInfo).to.exist() + expect(keyInfo).to.have.property('name', keyName) + expect(keyInfo).to.have.property('id') + }) + + it('does not overwrite existing key', async () => { + await expect(ks.createKey(keyName, 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('can export/import a key', async () => { + const keyName = 'a new key' + const password = 'my sneaky password' + const keyInfo = await ks.createKey(keyName, 'Ed25519') + const exportedKey = await ks.exportKey(keyName, password) + // remove it so we can import it + await ks.removeKey(keyName) + const importedKey = await ks.importKey(keyName, exportedKey, password) + expect(importedKey.id).to.eql(keyInfo.id) + }) + + it('cannot create the "self" key', async () => { + await expect(ks.createKey('self', 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + + describe('query', () => { + it('finds all existing keys', async () => { + const keys = await ks.listKeys() + expect(keys).to.exist() + const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) + expect(mykey).to.exist() + }) + + it('finds a key by name', async () => { + const key = await ks.findKeyByName(rsaKeyName) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) + + it('finds a key by id', async () => { + const key = await ks.findKeyById(rsaKeyInfo.id) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) + + it('returns the key\'s name and id', async () => { + const keys = await ks.listKeys() + expect(keys).to.exist() + keys.forEach((key) => { + expect(key).to.have.property('name') + expect(key).to.have.property('id') + }) + }) + }) + + describe('exported key', () => { + let pemKey: string + + it('requires the password', async () => { + // @ts-expect-error invalid parameters + await expect(ks.exportKey(rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_PASSWORD_REQUIRED') + }) + + it('requires the key name', async () => { + // @ts-expect-error invalid parameters + await expect(ks.exportKey(undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('is a PKCS #8 encrypted pem', async () => { + pemKey = await ks.exportKey(rsaKeyName, 'password') + expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) + + it('can be imported', async () => { + const key = await ks.importKey('imported-key', pemKey, 'password') + expect(key.name).to.equal('imported-key') + expect(key.id).to.equal(rsaKeyInfo.id) + }) + + it('requires the pem', async () => { + // @ts-expect-error invalid parameters + await expect(ks.importKey('imported-key', undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_PEM_REQUIRED') + }) + + it('cannot be imported as an existing key name', async () => { + await expect(ks.importKey(rsaKeyName, pemKey, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot be imported with the wrong password', async () => { + await expect(ks.importKey('a-new-name-for-import', pemKey, 'not the password')).to.eventually.be.rejected.with.property('code', 'ERR_CANNOT_READ_KEY') + }) + }) + + describe('peer id', () => { + const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==' + let alice: PeerId + + before(async function () { + const encoded = uint8ArrayFromString(alicePrivKey, 'base64pad') + const privateKey = await unmarshalPrivateKey(encoded) + alice = await createFromPrivKey(privateKey) + }) + + it('private key can be imported', async () => { + const key = await ks.importPeer('alice', alice) + expect(key.name).to.equal('alice') + expect(key.id).to.equal(alice.toString()) + }) + + it('private key can be exported', async () => { + const alice2 = await ks.exportPeerId('alice') + + expect(alice.equals(alice2)).to.be.true() + expect(alice2).to.have.property('privateKey').that.is.ok() + expect(alice2).to.have.property('publicKey').that.is.ok() + }) + + it('private key import requires a valid name', async () => { + // @ts-expect-error invalid parameters + await expect(ks.importPeer(undefined, alice)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('private key import requires the peer', async () => { + // @ts-expect-error invalid parameters + await expect(ks.importPeer('alice')).to.eventually.be.rejected.with.property('code', 'ERR_MISSING_PRIVATE_KEY') + }) + + it('key id exists', async () => { + const key = await ks.findKeyById(alice.toString()) + expect(key).to.exist() + expect(key).to.have.property('name', 'alice') + expect(key).to.have.property('id', alice.toString()) + }) + + it('key name exists', async () => { + const key = await ks.findKeyByName('alice') + expect(key).to.exist() + expect(key).to.have.property('name', 'alice') + expect(key).to.have.property('id', alice.toString()) + }) + + it('can create Ed25519 peer id', async () => { + const name = 'ed-key' + await ks.createKey(name, 'Ed25519') + const peer = await ks.exportPeerId(name) + + expect(peer).to.have.property('type', 'Ed25519') + expect(peer).to.have.property('privateKey').that.is.ok() + expect(peer).to.have.property('publicKey').that.is.ok() + }) + + it('can create RSA peer id', async () => { + const name = 'rsa-key' + await ks.createKey(name, 'RSA', 2048) + const peer = await ks.exportPeerId(name) + + expect(peer).to.have.property('type', 'RSA') + expect(peer).to.have.property('privateKey').that.is.ok() + expect(peer).to.have.property('publicKey').that.is.ok() + }) + + it('can create secp256k1 peer id', async () => { + const name = 'secp256k1-key' + await ks.createKey(name, 'secp256k1') + const peer = await ks.exportPeerId(name) + + expect(peer).to.have.property('type', 'secp256k1') + expect(peer).to.have.property('privateKey').that.is.ok() + expect(peer).to.have.property('publicKey').that.is.ok() + }) + }) + + describe('rename', () => { + it('requires an existing key name', async () => { + await expect(ks.renameKey('not-there', renamedRsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_NOT_FOUND') + }) + + it('requires a valid new key name', async () => { + await expect(ks.renameKey(rsaKeyName, '..\not-valid')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') + }) + + it('does not overwrite existing key', async () => { + await expect(ks.renameKey(rsaKeyName, rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot create the "self" key', async () => { + await expect(ks.renameKey(rsaKeyName, 'self')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') + }) + + it('removes the existing key name', async () => { + const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + // Try to find the changed key + await expect(ks.findKeyByName(rsaKeyName)).to.eventually.be.rejected() + }) + + it('creates the new key name', async () => { + const key = await ks.findKeyByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + }) + + it('does not change the key ID', async () => { + const key = await ks.findKeyByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + }) + + it('throws with invalid key names', async () => { + // @ts-expect-error invalid parameters + await expect(ks.findKeyByName(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + + describe('key removal', () => { + it('cannot remove the "self" key', async () => { + await expect(ks.removeKey('self')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('cannot remove an unknown key', async () => { + await expect(ks.removeKey('not-there')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('can remove a known key', async () => { + const key = await ks.removeKey(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + }) + }) + + describe('rotate keychain passphrase', () => { + let oldPass: string + let kc: KeyChain + let options: KeyChainInit + let ds: Datastore + before(async () => { + ds = new MemoryDatastore() + oldPass = `hello-${Date.now()}-${Date.now()}` + options = { + pass: oldPass, + dek: { + salt: '3Nd/Ya4ENB3bcByNKptb4IR', + iterationCount: 10000, + keyLength: 64, + hash: 'sha2-512' + } + } + kc = new DefaultKeyChain({ + datastore: ds + }, options) + }) + + it('should validate newPass is a string', async () => { + // @ts-expect-error invalid parameters + await expect(kc.rotateKeychainPass(oldPass, 1234567890)).to.eventually.be.rejected() + }) + + it('should validate oldPass is a string', async () => { + // @ts-expect-error invalid parameters + await expect(kc.rotateKeychainPass(1234, 'newInsecurePassword1')).to.eventually.be.rejected() + }) + + it('should validate newPass is at least 20 characters', async () => { + try { + await kc.rotateKeychainPass(oldPass, 'not20Chars') + } catch (err: any) { + expect(err).to.exist() + } + }) + + it('can rotate keychain passphrase', async () => { + await kc.createKey('keyCreatedWithOldPassword', 'RSA', 2048) + await kc.rotateKeychainPass(oldPass, 'newInsecurePassphrase') + + // Get Key PEM from datastore + const dsname = new Key('/pkcs8/' + 'keyCreatedWithOldPassword') + const res = await ds.get(dsname) + const pem = uint8ArrayToString(res) + + const oldDek = options.pass != null + ? pbkdf2( + options.pass, + options.dek?.salt ?? 'salt', + options.dek?.iterationCount ?? 0, + options.dek?.keyLength ?? 0, + options.dek?.hash ?? 'sha2-256' + ) + : '' + + const newDek = pbkdf2( + 'newInsecurePassphrase', + options.dek?.salt ?? 'salt', + options.dek?.iterationCount ?? 0, + options.dek?.keyLength ?? 0, + options.dek?.hash ?? 'sha2-256' + ) + + // Dek with old password should not work: + await expect(kc.importKey('keyWhosePassChanged', pem, oldDek)) + .to.eventually.be.rejected() + // Dek with new password should work: + await expect(kc.importKey('keyWhosePasswordChanged', pem, newDek)) + .to.eventually.have.property('name', 'keyWhosePasswordChanged') + }).timeout(10000) + }) +}) + +describe('libp2p.keychain', () => { + it('needs a passphrase to be used, otherwise throws an error', async () => { + expect(() => { + return new DefaultKeyChain({ + datastore: new MemoryDatastore() + }, { + pass: '' + }) + }).to.throw() + }) + + it('can be used when a passphrase is provided', async () => { + const keychain = new DefaultKeyChain({ + datastore: new MemoryDatastore() + }, { + pass: '12345678901234567890' + }) + + const kInfo = await keychain.createKey('keyName', 'Ed25519') + expect(kInfo).to.exist() + }) + + it('can reload keys', async () => { + const datastore = new MemoryDatastore() + const keychain = new DefaultKeyChain({ + datastore + }, { + pass: '12345678901234567890' + }) + + const kInfo = await keychain.createKey('keyName', 'Ed25519') + expect(kInfo).to.exist() + + const keychain2 = new DefaultKeyChain({ + datastore + }, { + pass: '12345678901234567890' + }) + + const key = await keychain2.findKeyByName('keyName') + + expect(key).to.exist() + }) +}) diff --git a/packages/keychain/test/peerid.spec.ts b/packages/keychain/test/peerid.spec.ts new file mode 100644 index 0000000000..cc8e8a5fcf --- /dev/null +++ b/packages/keychain/test/peerid.spec.ts @@ -0,0 +1,76 @@ +/* eslint-env mocha */ + +import { supportedKeys, unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { createFromPrivKey } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { base58btc } from 'multiformats/bases/base58' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { PeerId } from '@libp2p/interface-peer-id' + +const sample = { + id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', + privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=' +} + +describe('peer ID', () => { + let peer: PeerId + let publicKeyDer: Uint8Array // a buffer + + before(async () => { + const encoded = uint8ArrayFromString(sample.privKey, 'base64pad') + peer = await createFromPrivKey(await unmarshalPrivateKey(encoded)) + }) + + it('decoded public key', async () => { + if (peer.publicKey == null) { + throw new Error('PublicKey missing from PeerId') + } + + if (peer.privateKey == null) { + throw new Error('PrivateKey missing from PeerId') + } + + // get protobuf version of the public key + const publicKeyProtobuf = peer.publicKey + const publicKey = unmarshalPublicKey(publicKeyProtobuf) + publicKeyDer = publicKey.marshal() + + // get protobuf version of the private key + const privateKeyProtobuf = peer.privateKey + const key = await unmarshalPrivateKey(privateKeyProtobuf) + expect(key).to.exist() + }) + + it('encoded public key with DER', async () => { + const rsa = supportedKeys.rsa.unmarshalRsaPublicKey(publicKeyDer) + const keyId = await rsa.hash() + const kids = base58btc.encode(keyId).substring(1) + expect(kids).to.equal(peer.toString()) + }) + + it('encoded public key with JWT', async () => { + const jwk = { + kty: 'RSA', + n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw', + e: 'AQAB', + alg: 'RS256', + kid: '2011-04-29' + } + const rsa = new supportedKeys.rsa.RsaPublicKey(jwk) + const keyId = await rsa.hash() + const kids = base58btc.encode(keyId).substring(1) + expect(kids).to.equal(peer.toString()) + }) + + it('decoded private key', async () => { + if (peer.privateKey == null) { + throw new Error('PrivateKey missing from PeerId') + } + + // get protobuf version of the private key + const privateKeyProtobuf = peer.privateKey + const key = await unmarshalPrivateKey(privateKeyProtobuf) + expect(key).to.exist() + }) +}) diff --git a/packages/keychain/tsconfig.json b/packages/keychain/tsconfig.json new file mode 100644 index 0000000000..f3aa4228a3 --- /dev/null +++ b/packages/keychain/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-keychain" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index 546978e727..83cb7e33c7 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -115,7 +115,7 @@ }, "dependencies": { "@achingbrain/nat-port-mapper": "^1.0.9", - "@libp2p/crypto": "^1.0.17", + "@libp2p/crypto": "^1.0.0", "@libp2p/interface-address-manager": "^3.0.0", "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-connection-encrypter": "^4.0.0", @@ -137,23 +137,23 @@ "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", "@libp2p/keychain": "^2.0.0", - "@libp2p/logger": "^2.1.1", - "@libp2p/multistream-select": "^3.1.8", - "@libp2p/peer-collections": "^3.0.1", + "@libp2p/logger": "^2.0.0", + "@libp2p/multistream-select": "^3.0.0", + "@libp2p/peer-collections": "^3.0.0", "@libp2p/peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", "@libp2p/peer-record": "^5.0.0", - "@libp2p/peer-store": "^8.2.0", - "@libp2p/topology": "^4.0.1", + "@libp2p/peer-store": "^8.0.0", + "@libp2p/topology": "^4.0.0", "@libp2p/tracked-map": "^3.0.0", - "@libp2p/utils": "^3.0.10", - "@multiformats/mafmt": "^12.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", - "datastore-core": "^9.0.0", - "interface-datastore": "^8.0.0", - "it-all": "^3.0.1", + "datastore-core": "^9.0.1", + "interface-datastore": "^8.2.0", + "it-all": "^3.0.2", "it-drain": "^3.0.1", "it-filter": "^3.0.1", "it-first": "^3.0.1", @@ -167,7 +167,7 @@ "it-pipe": "^3.0.1", "it-stream-types": "^2.0.1", "merge-options": "^3.0.4", - "multiformats": "^11.0.0", + "multiformats": "^11.0.2", "p-defer": "^4.0.0", "p-queue": "^7.3.4", "p-retry": "^5.0.0", @@ -175,8 +175,8 @@ "protons-runtime": "^5.0.0", "rate-limiter-flexible": "^2.3.11", "uint8arraylist": "^2.4.3", - "uint8arrays": "^4.0.2", - "wherearewe": "^2.0.0", + "uint8arrays": "^4.0.3", + "wherearewe": "^2.0.1", "xsalsa20": "^1.1.0" }, "devDependencies": { @@ -192,27 +192,27 @@ "@libp2p/interface-connection-encrypter-compliance-tests": "^5.0.0", "@libp2p/interface-mocks": "^12.0.0", "@libp2p/interop": "^8.0.0", - "@libp2p/kad-dht": "^9.2.0", + "@libp2p/kad-dht": "^9.0.0", "@libp2p/mdns": "^8.0.0", - "@libp2p/mplex": "^8.0.1", + "@libp2p/mplex": "^8.0.0", "@libp2p/pubsub": "^7.0.1", - "@libp2p/tcp": "^7.0.1", - "@libp2p/websockets": "^6.0.1", + "@libp2p/tcp": "^7.0.0", + "@libp2p/websockets": "^6.0.0", "@types/varint": "^6.0.0", "@types/xsalsa20": "^1.1.0", - "aegir": "^39.0.5", - "cborg": "^1.8.1", + "aegir": "^39.0.10", + "cborg": "^2.0.1", "delay": "^6.0.0", "execa": "^7.0.0", - "go-libp2p": "^1.0.1", + "go-libp2p": "^1.1.1", "it-pushable": "^3.0.0", "it-to-buffer": "^4.0.1", "npm-run-all": "^4.1.5", - "p-event": "^5.0.1", + "p-event": "^6.0.0", "p-times": "^4.0.0", "p-wait-for": "^5.0.0", "protons": "^7.0.2", - "sinon": "^15.0.1", + "sinon": "^15.1.0", "sinon-ts": "^1.0.0" }, "browser": { diff --git a/packages/libp2p/tsconfig.json b/packages/libp2p/tsconfig.json index 9ac0d9429c..e4667a71b2 100644 --- a/packages/libp2p/tsconfig.json +++ b/packages/libp2p/tsconfig.json @@ -8,6 +8,9 @@ "test" ], "references": [ + { + "path": "../crypto" + }, { "path": "../interface-address-manager" }, @@ -79,6 +82,57 @@ }, { "path": "../interfaces" + }, + { + "path": "../kad-dht" + }, + { + "path": "../keychain" + }, + { + "path": "../logger" + }, + { + "path": "../multistream-select" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-discovery-bootstrap" + }, + { + "path": "../peer-discovery-mdns" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../peer-record" + }, + { + "path": "../peer-store" + }, + { + "path": "../stream-multiplexer-mplex" + }, + { + "path": "../topology" + }, + { + "path": "../tracked-map" + }, + { + "path": "../transport-tcp" + }, + { + "path": "../transport-websockets" + }, + { + "path": "../utils" } ] } diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md new file mode 100644 index 0000000000..a05e46a74a --- /dev/null +++ b/packages/logger/CHANGELOG.md @@ -0,0 +1,184 @@ +## [2.1.1](https://github.com/libp2p/js-libp2p-logger/compare/v2.1.0...v2.1.1) (2023-06-02) + + +### Bug Fixes + +* 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)) + +## [2.1.0](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.7...v2.1.0) (2023-05-31) + + +### Features + +* added multiaddr formatter to logging ([#34](https://github.com/libp2p/js-libp2p-logger/issues/34)) ([1051708](https://github.com/libp2p/js-libp2p-logger/commit/1051708697299a51bfb9dac77bdd14f644ac0fe2)) + +## [2.0.7](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.6...v2.0.7) (2023-03-24) + + +### Bug Fixes + +* disable trace logging by default ([#32](https://github.com/libp2p/js-libp2p-logger/issues/32)) ([47915fe](https://github.com/libp2p/js-libp2p-logger/commit/47915fe85dc8b50713c4344d15cb9082d8147b8f)) + +## [2.0.6](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.5...v2.0.6) (2023-03-13) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([a24089e](https://github.com/libp2p/js-libp2p-logger/commit/a24089e1ad4bf47185828a880879fa60adf867d6)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([36c0fe1](https://github.com/libp2p/js-libp2p-logger/commit/36c0fe124ebd82c06c167b999d960343a4601468)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([e73d815](https://github.com/libp2p/js-libp2p-logger/commit/e73d8153a0b93a25b2fbdfd6950eaa41e778ab4f)) + + +### Dependencies + +* update interface-datastore and aegir ([#29](https://github.com/libp2p/js-libp2p-logger/issues/29)) ([451d936](https://github.com/libp2p/js-libp2p-logger/commit/451d936b77b6e4588e31665e32f98e13da6c1899)) + +## [2.0.5](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.4...v2.0.5) (2023-01-06) + + +### Documentation + +* publish typedocs ([#18](https://github.com/libp2p/js-libp2p-logger/issues/18)) ([ff0a8ce](https://github.com/libp2p/js-libp2p-logger/commit/ff0a8ce535474bb30e7977fa7684bb4c17181ead)) + +## [2.0.4](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.3...v2.0.4) (2023-01-06) + + +### Dependencies + +* update @libp2p/interface-peer-id to 2.x.x ([#17](https://github.com/libp2p/js-libp2p-logger/issues/17)) ([9107f4e](https://github.com/libp2p/js-libp2p-logger/commit/9107f4ed0e8b5db713572b0c528b6860d6f572c3)) + +## [2.0.3](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.2...v2.0.3) (2023-01-06) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#16](https://github.com/libp2p/js-libp2p-logger/issues/16)) ([892f906](https://github.com/libp2p/js-libp2p-logger/commit/892f906e884af0fd4af0f5cd7e01dc1dceacad6f)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.1...v2.0.2) (2022-10-12) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([0d54ade](https://github.com/libp2p/js-libp2p-logger/commit/0d54ade029fcf26d77b35b043e7a521088930231)) +* update project config ([#11](https://github.com/libp2p/js-libp2p-logger/issues/11)) ([e993132](https://github.com/libp2p/js-libp2p-logger/commit/e9931326baa5de5ff8614d685413e1f5bb2bf0bc)) + + +### Dependencies + +* bump multiformats from 9.9.0 to 10.0.0 ([#10](https://github.com/libp2p/js-libp2p-logger/issues/10)) ([1cbf8c9](https://github.com/libp2p/js-libp2p-logger/commit/1cbf8c973605316f9560c1a50e2a19543ec36f26)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.0...v2.0.1) (2022-08-12) + + +### Trivial Changes + +* **deps:** bump interface-datastore from 6.1.1 to 7.0.0 ([#7](https://github.com/libp2p/js-libp2p-logger/issues/7)) ([13aac56](https://github.com/libp2p/js-libp2p-logger/commit/13aac5661ecf77cd5a6d79eb92008435249eb428)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-logger/compare/v1.1.6...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p dependencies ([#4](https://github.com/libp2p/js-libp2p-logger/issues/4)) ([7775b9b](https://github.com/libp2p/js-libp2p-logger/commit/7775b9b2a51be5672bdc2640da85cae0510ef6bd)) + +### [1.1.6](https://github.com/libp2p/js-libp2p-logger/compare/v1.1.5...v1.1.6) (2022-06-09) + + +### Trivial Changes + +* add badge ([#1](https://github.com/libp2p/js-libp2p-logger/issues/1)) ([3109e30](https://github.com/libp2p/js-libp2p-logger/commit/3109e30508e32a8e24bd921afcd1b18df57483cd)) +* fix release ([3f850bc](https://github.com/libp2p/js-libp2p-logger/commit/3f850bc6d1c11248a6afb1d712e0074cf4b705af)) +* **release:** 1.0.0 [skip ci] ([91cfcba](https://github.com/libp2p/js-libp2p-logger/commit/91cfcba8635434262b11c40fe077c359e842e973)), closes [#1](https://github.com/libp2p/js-libp2p-logger/issues/1) + +## [@libp2p/logger-v1.1.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.4...@libp2p/logger-v1.1.5) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/logger-v1.1.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.3...@libp2p/logger-v1.1.4) (2022-04-14) + + +### Bug Fixes + +* add logger methods, fix peer id deserialization ([#194](https://github.com/libp2p/js-libp2p-interfaces/issues/194)) ([f0e1fad](https://github.com/libp2p/js-libp2p-interfaces/commit/f0e1fad42701d73eef4233ec2b9a8aafa0b2ab96)) + +## [@libp2p/logger-v1.1.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.2...@libp2p/logger-v1.1.3) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/logger-v1.1.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.1...@libp2p/logger-v1.1.2) (2022-03-15) + + +### 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)) + +## [@libp2p/logger-v1.1.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.0...@libp2p/logger-v1.1.1) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/logger-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.4...@libp2p/logger-v1.1.0) (2022-02-26) + + +### Features + +* add trace option to logger ([#177](https://github.com/libp2p/js-libp2p-interfaces/issues/177)) ([19774eb](https://github.com/libp2p/js-libp2p-interfaces/commit/19774ebe05cc4ff8c8200dfdde046016abf5d19e)) + +## [@libp2p/logger-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.3...@libp2p/logger-v1.0.4) (2022-02-21) + + +### Bug Fixes + +* remove unused dht query option ([#176](https://github.com/libp2p/js-libp2p-interfaces/issues/176)) ([e0ce46d](https://github.com/libp2p/js-libp2p-interfaces/commit/e0ce46d371a92a7063f02e7a1729a39def80e15e)) + +## [@libp2p/logger-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.2...@libp2p/logger-v1.0.3) (2022-02-11) + + +### Bug Fixes + +* log cids ([#165](https://github.com/libp2p/js-libp2p-interfaces/issues/165)) ([68831e8](https://github.com/libp2p/js-libp2p-interfaces/commit/68831e804630e1f45ffee56a7585af62072d7145)) + +## [@libp2p/logger-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.1...@libp2p/logger-v1.0.2) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/logger-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.0...@libp2p/logger-v1.0.1) (2022-02-07) + + +### Bug Fixes + +* add logging formatters ([#159](https://github.com/libp2p/js-libp2p-interfaces/issues/159)) ([2fa518c](https://github.com/libp2p/js-libp2p-interfaces/commit/2fa518c7489dcd31d5b28f79114dfdc94133d784)) + +## @libp2p/logger-v1.0.0 (2022-02-07) + + +### Features + +* add logger package ([#158](https://github.com/libp2p/js-libp2p-interfaces/issues/158)) ([f327cd2](https://github.com/libp2p/js-libp2p-interfaces/commit/f327cd24825d9ce2f45a02fdb9b47c9735c847e0)) + +## @libp2p/tracked-map-v1.0.0 (2022-02-05) + + +### Features + +* 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/LICENSE b/packages/logger/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/logger/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/logger/LICENSE-APACHE b/packages/logger/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/logger/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/logger/LICENSE-MIT b/packages/logger/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/logger/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 0000000000..def89c67a1 --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,77 @@ +# @libp2p/logger + +[![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) + +> A logging component for use in js-libp2p modules + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +A map that reports it's size to the libp2p [Metrics](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/metrics#readme) system. + +If metrics are disabled a regular map is used. + +## Example + +```JavaScript +import { logger } from '@libp2p/logger' + +const log = logger('libp2p:my:component:name') + +log('something happened: %s', 'it was ok') +log.error('something bad happened: %o', err) + +log('with this peer: %p', aPeerId) +log('and this base58btc: %b', aUint8Array) +log('and this base32: %t', aUint8Array) +``` + +```console +$ DEBUG=libp2p:* node index.js +something happened: it was ok +something bad happened: +with this peer: 12D3Foo +with this base58btc: Qmfoo +with this base32: bafyfoo +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000000..0a24ab4199 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,67 @@ +{ + "name": "@libp2p/logger", + "version": "2.1.1", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "debug": "^4.3.4", + "interface-datastore": "^8.2.0", + "multiformats": "^11.0.2" + }, + "devDependencies": { + "@libp2p/peer-id": "^2.0.0", + "@types/debug": "^4.1.7", + "aegir": "^39.0.10", + "sinon": "^15.1.0", + "uint8arrays": "^4.0.3" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000000..c50728146c --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,90 @@ +import debug from 'debug' +import { base32 } from 'multiformats/bases/base32' +import { base58btc } from 'multiformats/bases/base58' +import { base64 } from 'multiformats/bases/base64' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Key } from 'interface-datastore' +import type { CID } from 'multiformats/cid' + +// Add a formatter for converting to a base58 string +debug.formatters.b = (v?: Uint8Array): string => { + return v == null ? 'undefined' : base58btc.baseEncode(v) +} + +// Add a formatter for converting to a base32 string +debug.formatters.t = (v?: Uint8Array): string => { + return v == null ? 'undefined' : base32.baseEncode(v) +} + +// Add a formatter for converting to a base64 string +debug.formatters.m = (v?: Uint8Array): string => { + return v == null ? 'undefined' : base64.baseEncode(v) +} + +// Add a formatter for stringifying peer ids +debug.formatters.p = (v?: PeerId): string => { + return v == null ? 'undefined' : v.toString() +} + +// Add a formatter for stringifying CIDs +debug.formatters.c = (v?: CID): string => { + return v == null ? 'undefined' : v.toString() +} + +// Add a formatter for stringifying Datastore keys +debug.formatters.k = (v: Key): string => { + return v == null ? 'undefined' : v.toString() +} + +// Add a formatter for stringifying Multiaddrs +debug.formatters.a = (v?: Multiaddr): string => { + return v == null ? 'undefined' : v.toString() +} + +export interface Logger { + (formatter: any, ...args: any[]): void + error: (formatter: any, ...args: any[]) => void + trace: (formatter: any, ...args: any[]) => void + enabled: boolean +} + +function createDisabledLogger (namespace: string): debug.Debugger { + const logger = (): void => {} + logger.enabled = false + logger.color = '' + logger.diff = 0 + logger.log = (): void => {} + logger.namespace = namespace + logger.destroy = () => true + logger.extend = () => logger + + return logger +} + +export function logger (name: string): Logger { + // trace logging is a no-op by default + let trace: debug.Debugger = createDisabledLogger(`${name}:trace`) + + // look at all the debug names and see if trace logging has explicitly been enabled + if (debug.enabled(`${name}:trace`) && debug.names.map(r => r.toString()).find(n => n.includes(':trace')) != null) { + trace = debug(`${name}:trace`) + } + + return Object.assign(debug(name), { + error: debug(`${name}:error`), + trace + }) +} + +export function disable (): void { + debug.disable() +} + +export function enable (namespaces: string): void { + debug.enable(namespaces) +} + +export function enabled (namespaces: string): boolean { + return debug.enabled(namespaces) +} diff --git a/packages/logger/test/index.spec.ts b/packages/logger/test/index.spec.ts new file mode 100644 index 0000000000..8b8b1b6731 --- /dev/null +++ b/packages/logger/test/index.spec.ts @@ -0,0 +1,134 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import debug from 'debug' +import { Key } from 'interface-datastore' +import { base32 } from 'multiformats/bases/base32' +import { base58btc } from 'multiformats/bases/base58' +import { base64 } from 'multiformats/bases/base64' +import sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as unint8ArrayToString } from 'uint8arrays/to-string' +import { logger } from '../src/index.js' + +describe('logger', () => { + it('creates a logger', () => { + const log = logger('hello') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.not.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.not.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.not.true() + }) + + it('creates a logger with logging enabled', () => { + debug.enable('enabled-logger') + + const log = logger('enabled-logger') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.not.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.not.true() + }) + + it('creates a logger with logging and errors enabled', () => { + debug.enable('enabled-with-error-logger*') + + const log = logger('enabled-with-error-logger') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.not.true() + }) + + it('creates a logger with trace enabled', () => { + debug.enable('enabled-with-trace-logger*,*:trace') + + const log = logger('enabled-with-trace-logger') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.true() + }) + + it('has all formatters', () => { + debug.enable('enabled-with-formatters') + + expect(debug.formatters).to.have.property('b').that.is.a('function') + expect(debug.formatters).to.have.property('t').that.is.a('function') + expect(debug.formatters).to.have.property('m').that.is.a('function') + expect(debug.formatters).to.have.property('p').that.is.a('function') + expect(debug.formatters).to.have.property('c').that.is.a('function') + expect(debug.formatters).to.have.property('k').that.is.a('function') + expect(debug.formatters).to.have.property('a').that.is.a('function') + }) + + it('test printf style formatting', () => { + const log = logger('printf-style') + debug.enable('printf-style') + + const ma = multiaddr('/ip4/127.0.0.1/tcp/4001') + + const debugSpy = sinon.spy(debug, 'log') + + log('multiaddr %a', ma) + + expect(debugSpy.firstCall.args[0], 'Multiaddr formatting not included').to.include(`multiaddr ${ma.toString()}`) + }) + + it('test ma formatter', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/4001') + + expect(debug.formatters.a(ma)).to.equal(ma.toString()) + }) + + it('test peerId formatter', () => { + const peerId = peerIdFromString('QmZ8eiDPqQqWR17EPxiwCDgrKPVhCHLcyn6xSCNpFAdAZb') + + expect(debug.formatters.p(peerId)).to.equal(peerId.toString()) + }) + + it('test cid formatter', () => { + const peerId = peerIdFromString('QmZ8eiDPqQqWR17EPxiwCDgrKPVhCHLcyn6xSCNpFAdAZb') + const cid = peerId.toCID() + + expect(debug.formatters.c(cid)).to.equal(cid.toString()) + }) + + it('test base58 formatter', () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + + expect(debug.formatters.b(buf)).to.equal(base58btc.baseEncode(buf)) + }) + + it('test base32 formatter', () => { + const buf = uint8ArrayFromString('jbswy3dpfqqho33snrscc===', 'base32') + + expect(debug.formatters.t(buf)).to.equal(base32.baseEncode(buf)) + }) + + it('test base64 formatter', () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base64') + + expect(debug.formatters.m(buf)).to.equal(base64.baseEncode(buf)) + }) + + it('test datastore key formatter', () => { + const buf = uint8ArrayFromString('jbswy3dpfqqho33snrscc===', 'base32') + + const key = new Key('/' + unint8ArrayToString(buf, 'base32'), false) + + expect(debug.formatters.k(key)).to.equal(key.toString()) + }) +}) diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000000..38a80e7a63 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/metrics-prometheus/.aegir.js b/packages/metrics-prometheus/.aegir.js new file mode 100644 index 0000000000..000760bbdb --- /dev/null +++ b/packages/metrics-prometheus/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} \ No newline at end of file diff --git a/packages/metrics-prometheus/CHANGELOG.md b/packages/metrics-prometheus/CHANGELOG.md new file mode 100644 index 0000000000..237f9f466c --- /dev/null +++ b/packages/metrics-prometheus/CHANGELOG.md @@ -0,0 +1,86 @@ +## [1.1.5](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.4...v1.1.5) (2023-05-23) + + +### Bug Fixes + +* 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)) + +## [1.1.4](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.3...v1.1.4) (2023-05-12) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7756331](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/77563319cdb0edcc75be7cd4ad7758054595991b)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1a3861f](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/1a3861fd7c76a8fa296ff3aad39de633aecf3570)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([b66c4a0](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/b66c4a08983c1bbb2d1285aa2ea749ae00088643)) + + +### Documentation + +* added examples for package documentation for methods ([#31](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/31)) ([7dbd895](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/7dbd895dbf75f98b5730ad750b3e1aa9bc676c77)), closes [#30](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/30) + +## [1.1.3](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.2...v1.1.3) (2022-12-16) + + +### Documentation + +* publish api docs ([#14](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/14)) ([78e708f](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/78e708f4a4b5040988da90ecfb636a1e59a96ee4)) + +## [1.1.2](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.1...v1.1.2) (2022-11-22) + + +### Bug Fixes + +* use collectDefaultMetrics option ([#7](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/7)) ([3e4f00c](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/3e4f00c539da19bdfd8a26d335f00a2457545b53)) + +## [1.1.1](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.0...v1.1.1) (2022-11-21) + + +### Bug Fixes + +* allow multiple consumers of metrics ([#6](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/6)) ([92bde9b](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/92bde9b8d9a533c4e8aca6a98c02fa1bdc37156e)) + +## [1.1.0](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.0.1...v1.1.0) (2022-11-21) + + +### Features + +* register metrics with custom registry ([#4](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/4)) ([5da2897](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/5da289702186b73862cce39ecd1752792e6f9751)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([351b00c](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/351b00cad878cd3269a18da3f725613f991a83ae)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.0.0...v1.0.1) (2022-11-05) + + +### Bug Fixes + +* pass numbers to prom-client ([#1](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/1)) ([7c38140](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/7c38140d97dc4cbfb5d21e63a214df133eae9d73)) + +## 1.0.0 (2022-11-05) + + +### Bug Fixes + +* add tests for counters ([627b5c5](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/627b5c5886380433ef30efc80e8d32700f478f0b)) +* update release config ([ee1542d](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/ee1542d18863b3d9d12cdb2a8ebb21241c61d993)) + + +### Trivial Changes + +* add components ([92fccf7](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/92fccf71ef23ccf9ca819ccc050ce12ae088ed76)) +* fix tests ([f3f72f6](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/f3f72f6229969a6e947408cd3ba67a6b20607394)) +* initial implementation ([b3a4d8b](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/b3a4d8b721b0974ce42a889d4d1029fe288553fe)) +* linting ([9758456](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/9758456c2cf6dee949967609dc88655a671d0b25)) +* linting ([0f84e4f](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/0f84e4f796801b87def8d4718e1e150a2af29065)) +* simplified metrics ([12e6077](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/12e6077318155bc844a0d100f1c00d1bf7789111)) +* stricter name/label parsing and tests ([0cf651d](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/0cf651de02102f406a45411ceb044a3a116a7436)) +* update comments ([10b8c98](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/10b8c98718ff3579b1a2b236ed294c319bdc4ac4)) +* update readme ([4176169](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/41761694d442f561a425c7bf6963e49627e8204e)) + + +### Documentation + +* update readme to link to correct branch ([1a7565b](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/1a7565b5986ba689eb7a6d555b15ca1a4e4d3f31)) diff --git a/packages/metrics-prometheus/LICENSE b/packages/metrics-prometheus/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/metrics-prometheus/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/metrics-prometheus/LICENSE-APACHE b/packages/metrics-prometheus/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/metrics-prometheus/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/metrics-prometheus/LICENSE-MIT b/packages/metrics-prometheus/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/metrics-prometheus/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/metrics-prometheus/README.md b/packages/metrics-prometheus/README.md new file mode 100644 index 0000000000..cd5a86c3b4 --- /dev/null +++ b/packages/metrics-prometheus/README.md @@ -0,0 +1,97 @@ +# @libp2p/prometheus-metrics + +[![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) + +> Collect libp2p metrics for scraping by Prometheus or Graphana + +## Table of contents + +- [Install](#install) +- [Usage](#usage) + - [Queries](#queries) + - [Data sent/received](#data-sentreceived) + - [CPU usage](#cpu-usage) + - [Memory usage](#memory-usage) + - [DHT query time](#dht-query-time) + - [TCP transport dialer errors](#tcp-transport-dialer-errors) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/prometheus-metrics +``` + +## Usage + +Configure your libp2p node with Prometheus metrics: + +```js +import { createLibp2p } from 'libp2p' +import { prometheusMetrics } from '@libp2p/prometheus-metrics' + +const node = await createLibp2p({ + metrics: prometheusMetrics() +}) +``` + +Then use the `prom-client` module to supply metrics to the Prometheus/Graphana client using your http framework: + +```js +import client from 'prom-client' + +async handler (request, h) { + return h.response(await client.register.metrics()) + .type(client.register.contentType) +} +``` + +All Prometheus metrics are global so there's no other work required to extract them. + +### Queries + +Some useful queries are: + +#### Data sent/received + + rate(libp2p_data_transfer_bytes_total[30s]) + +#### CPU usage + + rate(process_cpu_user_seconds_total[30s]) * 100 + +#### Memory usage + + nodejs_memory_usage_bytes + +#### DHT query time + + libp2p_kad_dht_wan_query_time_seconds + +or + + libp2p_kad_dht_lan_query_time_seconds + +#### TCP transport dialer errors + + rate(libp2p_tcp_dialer_errors_total[30s]) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/metrics-prometheus/package.json b/packages/metrics-prometheus/package.json new file mode 100644 index 0000000000..a6c2fce62f --- /dev/null +++ b/packages/metrics-prometheus/package.json @@ -0,0 +1,64 @@ +{ + "name": "@libp2p/prometheus-metrics", + "version": "1.1.5", + "description": "Collect libp2p metrics for scraping by Prometheus or Graphana", + "author": "", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/metrics-prometheus#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t node", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main --cov" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/logger": "^2.0.0", + "it-foreach": "^2.0.3", + "it-stream-types": "^2.0.1", + "prom-client": "^14.1.0" + }, + "devDependencies": { + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "aegir": "^39.0.10", + "it-drain": "^3.0.2", + "it-pipe": "^3.0.1", + "p-defer": "^4.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/metrics-prometheus/src/counter-group.ts b/packages/metrics-prometheus/src/counter-group.ts new file mode 100644 index 0000000000..6cd52abdde --- /dev/null +++ b/packages/metrics-prometheus/src/counter-group.ts @@ -0,0 +1,58 @@ +import { Counter as PromCounter, type CollectFunction } from 'prom-client' +import { normaliseString, type CalculatedMetric } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { CounterGroup, CalculateMetric } from '@libp2p/interface-metrics' + +export class PrometheusCounterGroup implements CounterGroup, CalculatedMetric> { + private readonly counter: PromCounter + private readonly label: string + private readonly calculators: Array>> + + constructor (name: string, opts: PrometheusCalculatedMetricOptions>) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const label = this.label = normaliseString(opts.label ?? name) + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + await Promise.all(self.calculators.map(async calculate => { + const values = await calculate() + + Object.entries(values).forEach(([key, value]) => { + this.inc({ [label]: key }, value) + }) + })) + } + } + + this.counter = new PromCounter({ + name, + help, + labelNames: [this.label], + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric>): void { + this.calculators.push(calculator) + } + + increment (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + const inc = typeof value === 'number' ? value : 1 + + this.counter.inc({ [this.label]: key }, inc) + }) + } + + reset (): void { + this.counter.reset() + } +} diff --git a/packages/metrics-prometheus/src/counter.ts b/packages/metrics-prometheus/src/counter.ts new file mode 100644 index 0000000000..3dacf85322 --- /dev/null +++ b/packages/metrics-prometheus/src/counter.ts @@ -0,0 +1,50 @@ +import { type CollectFunction, Counter as PromCounter } from 'prom-client' +import { normaliseString, type CalculatedMetric } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { CalculateMetric, Counter } from '@libp2p/interface-metrics' + +export class PrometheusCounter implements Counter, CalculatedMetric { + private readonly counter: PromCounter + private readonly calculators: CalculateMetric[] + + constructor (name: string, opts: PrometheusCalculatedMetricOptions) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const labels = opts.label != null ? [normaliseString(opts.label)] : [] + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + const values = await Promise.all(self.calculators.map(async calculate => calculate())) + const sum = values.reduce((acc, curr) => acc + curr, 0) + + this.inc(sum) + } + } + + this.counter = new PromCounter({ + name, + help, + labelNames: labels, + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric): void { + this.calculators.push(calculator) + } + + increment (value: number = 1): void { + this.counter.inc(value) + } + + reset (): void { + this.counter.reset() + } +} diff --git a/packages/metrics-prometheus/src/index.ts b/packages/metrics-prometheus/src/index.ts new file mode 100644 index 0000000000..e8fad3b0bd --- /dev/null +++ b/packages/metrics-prometheus/src/index.ts @@ -0,0 +1,357 @@ +/** + * @packageDocumentation + * + * Collect libp2p metrics for scraping by Prometheus or Graphana. + * @module libp2p-prometheus-metrics + * + * A tracked metric can be created by calling either `registerMetric` on the metrics object + * + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * + * const metrics = prometheusMetrics()() + * const myMetric = metrics.registerMetric({ + * name: 'my_metric', + * label: 'my_label', + * help: 'my help text' + * }) + * + * myMetric.update(1) + * ``` + * A metric that is expensive to calculate can be created by passing a `calculate` function that will only be invoked when metrics are being scraped: + * + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * + * const metrics = prometheusMetrics()() + * const myMetric = metrics.registerMetric({ + * name: 'my_metric', + * label: 'my_label', + * help: 'my help text', + * calculate: async () => { + * // do something expensive + * return 1 + * } + * }) + * ``` + * + * If several metrics should be grouped together (e.g. for graphing purposes) `registerMetricGroup` can be used instead: + * + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * + * const metrics = prometheusMetrics()() + * const myMetricGroup = metrics.registerMetricGroup({ + * name: 'my_metric_group', + * label: 'my_label', + * help: 'my help text' + * }) + * + * myMetricGroup.increment({ my_label: 'my_value' }) + * ``` + * + * There are specific metric groups for tracking libp2p connections and streams: + * + * Track a newly opened multiaddr connection: + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * import { createLibp2p } from 'libp2p' + * + * + * const metrics = prometheusMetrics()() + * + * const libp2p = await createLibp2p({ + * metrics: metrics, + * }) + * // set up a multiaddr connection + * const connection = await libp2p.dial('multiaddr') + * const connections = metrics.trackMultiaddrConnection(connection) + * ``` + * + * Track a newly opened stream: + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * import { createLibp2p } from 'libp2p' + * + * const metrics = prometheusMetrics()() + * + * const libp2p = await createLibp2p({ + * metrics: metrics, + * }) + * + * const stream = await connection.newStream('/my/protocol') + * const streams = metrics.trackProtocolStream(stream) + * ``` + */ + +import { logger } from '@libp2p/logger' +import each from 'it-foreach' +import { collectDefaultMetrics, type DefaultMetricsCollectorConfiguration, register, type Registry } from 'prom-client' +import { PrometheusCounterGroup } from './counter-group.js' +import { PrometheusCounter } from './counter.js' +import { PrometheusMetricGroup } from './metric-group.js' +import { PrometheusMetric } from './metric.js' +import type { MultiaddrConnection, Stream, Connection } from '@libp2p/interface-connection' +import type { CalculatedMetricOptions, Counter, CounterGroup, Metric, MetricGroup, MetricOptions, Metrics } from '@libp2p/interface-metrics' +import type { Duplex, Source } from 'it-stream-types' + +const log = logger('libp2p:prometheus-metrics') + +// prom-client metrics are global +const metrics = new Map() + +export interface PrometheusMetricsInit { + /** + * Use a custom registry to register metrics. + * By default, the global registry is used to register metrics. + */ + registry?: Registry + + /** + * By default we collect default metrics - CPU, memory etc, to not do + * this, pass true here + */ + collectDefaultMetrics?: boolean + + /** + * prom-client options to pass to the `collectDefaultMetrics` function + */ + defaultMetrics?: DefaultMetricsCollectorConfiguration + + /** + * All metrics in prometheus are global so to prevent clashes in naming + * we reset the global metrics registry on creation - to not do this, + * pass true here + */ + preserveExistingMetrics?: boolean +} + +export interface PrometheusCalculatedMetricOptions extends CalculatedMetricOptions { + registry?: Registry +} + +class PrometheusMetrics implements Metrics { + private transferStats: Map + private readonly registry?: Registry + + constructor (init?: Partial) { + this.registry = init?.registry + + if (init?.preserveExistingMetrics !== true) { + log('Clearing existing metrics') + metrics.clear() + ;(this.registry ?? register).clear() + } + + if (init?.collectDefaultMetrics !== false) { + log('Collecting default metrics') + collectDefaultMetrics({ ...init?.defaultMetrics, register: this.registry ?? init?.defaultMetrics?.register }) + } + + // holds global and per-protocol sent/received stats + this.transferStats = new Map() + + log('Collecting data transfer metrics') + this.registerCounterGroup('libp2p_data_transfer_bytes_total', { + label: 'protocol', + calculate: () => { + const output: Record = {} + + for (const [key, value] of this.transferStats.entries()) { + output[key] = value + } + + // reset counts for next time + this.transferStats = new Map() + + return output + } + }) + + log('Collecting memory metrics') + this.registerMetricGroup('nodejs_memory_usage_bytes', { + label: 'memory', + calculate: () => { + return { + ...process.memoryUsage() + } + } + }) + } + + /** + * Increment the transfer stat for the passed key, making sure + * it exists first + */ + _incrementValue (key: string, value: number): void { + const existing = this.transferStats.get(key) ?? 0 + + this.transferStats.set(key, existing + value) + } + + /** + * Override the sink/source of the stream to count the bytes + * in and out + */ + _track (stream: Duplex>, name: string): void { + const self = this + + const sink = stream.sink + stream.sink = async function trackedSink (source) { + await sink(each(source, buf => { + self._incrementValue(`${name} sent`, buf.byteLength) + })) + } + + const source = stream.source + stream.source = each(source, buf => { + self._incrementValue(`${name} received`, buf.byteLength) + }) + } + + trackMultiaddrConnection (maConn: MultiaddrConnection): void { + this._track(maConn, 'global') + } + + trackProtocolStream (stream: Stream, connection: Connection): void { + if (stream.stat.protocol == null) { + // protocol not negotiated yet, should not happen as the upgrader + // calls this handler after protocol negotiation + return + } + + this._track(stream, stream.stat.protocol) + } + + registerMetric (name: string, opts: PrometheusCalculatedMetricOptions): void + registerMetric (name: string, opts?: MetricOptions): Metric + registerMetric (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Metric name is required') + } + + let metric = metrics.get(name) + + if (metrics.has(name)) { + log('Reuse existing metric', name) + + if (opts.calculate != null) { + metric.addCalculator(opts.calculate) + } + + return metrics.get(name) + } + + log('Register metric', name) + metric = new PrometheusMetric(name, { registry: this.registry, ...opts }) + + metrics.set(name, metric) + + if (opts.calculate == null) { + return metric + } + } + + registerMetricGroup (name: string, opts: PrometheusCalculatedMetricOptions>): void + registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup + registerMetricGroup (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Metric group name is required') + } + + let metricGroup = metrics.get(name) + + if (metricGroup != null) { + log('Reuse existing metric group', name) + + if (opts.calculate != null) { + metricGroup.addCalculator(opts.calculate) + } + + return metricGroup + } + + log('Register metric group', name) + metricGroup = new PrometheusMetricGroup(name, { registry: this.registry, ...opts }) + + metrics.set(name, metricGroup) + + if (opts.calculate == null) { + return metricGroup + } + } + + registerCounter (name: string, opts: PrometheusCalculatedMetricOptions): void + registerCounter (name: string, opts?: MetricOptions): Counter + registerCounter (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Counter name is required') + } + + let counter = metrics.get(name) + + if (counter != null) { + log('Reuse existing counter', name) + + if (opts.calculate != null) { + counter.addCalculator(opts.calculate) + } + + return metrics.get(name) + } + + log('Register counter', name) + counter = new PrometheusCounter(name, { registry: this.registry, ...opts }) + + metrics.set(name, counter) + + if (opts.calculate == null) { + return counter + } + } + + registerCounterGroup (name: string, opts: PrometheusCalculatedMetricOptions>): void + registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup + registerCounterGroup (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Counter group name is required') + } + + let counterGroup = metrics.get(name) + + if (counterGroup != null) { + log('Reuse existing counter group', name) + + if (opts.calculate != null) { + counterGroup.addCalculator(opts.calculate) + } + + return counterGroup + } + + log('Register counter group', name) + counterGroup = new PrometheusCounterGroup(name, { registry: this.registry, ...opts }) + + metrics.set(name, counterGroup) + + if (opts.calculate == null) { + return counterGroup + } + } +} + +export function prometheusMetrics (init?: Partial): () => Metrics { + return () => { + return new PrometheusMetrics(init) + } +} diff --git a/packages/metrics-prometheus/src/metric-group.ts b/packages/metrics-prometheus/src/metric-group.ts new file mode 100644 index 0000000000..302def85fc --- /dev/null +++ b/packages/metrics-prometheus/src/metric-group.ts @@ -0,0 +1,78 @@ +import { type CollectFunction, Gauge } from 'prom-client' +import { normaliseString, type CalculatedMetric } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { CalculateMetric, MetricGroup, StopTimer } from '@libp2p/interface-metrics' + +export class PrometheusMetricGroup implements MetricGroup, CalculatedMetric> { + private readonly gauge: Gauge + private readonly label: string + private readonly calculators: Array>> + + constructor (name: string, opts: PrometheusCalculatedMetricOptions>) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const label = this.label = normaliseString(opts.label ?? name) + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + await Promise.all(self.calculators.map(async calculate => { + const values = await calculate() + + Object.entries(values).forEach(([key, value]) => { + this.set({ [label]: key }, value) + }) + })) + } + } + + this.gauge = new Gauge({ + name, + help, + labelNames: [this.label], + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric>): void { + this.calculators.push(calculator) + } + + update (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + this.gauge.set({ [this.label]: key }, value) + }) + } + + increment (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + const inc = typeof value === 'number' ? value : 1 + + this.gauge.inc({ [this.label]: key }, inc) + }) + } + + decrement (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + const dec = typeof value === 'number' ? value : 1 + + this.gauge.dec({ [this.label]: key }, dec) + }) + } + + reset (): void { + this.gauge.reset() + } + + timer (key: string): StopTimer { + return this.gauge.startTimer({ + key: 0 + }) + } +} diff --git a/packages/metrics-prometheus/src/metric.ts b/packages/metrics-prometheus/src/metric.ts new file mode 100644 index 0000000000..bc65b5c916 --- /dev/null +++ b/packages/metrics-prometheus/src/metric.ts @@ -0,0 +1,62 @@ +import { type CollectFunction, Gauge } from 'prom-client' +import { normaliseString } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { Metric, StopTimer, CalculateMetric } from '@libp2p/interface-metrics' + +export class PrometheusMetric implements Metric { + private readonly gauge: Gauge + private readonly calculators: CalculateMetric[] + + constructor (name: string, opts: PrometheusCalculatedMetricOptions) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const labels = opts.label != null ? [normaliseString(opts.label)] : [] + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + const values = await Promise.all(self.calculators.map(async calculate => calculate())) + const sum = values.reduce((acc, curr) => acc + curr, 0) + + this.set(sum) + } + } + + this.gauge = new Gauge({ + name, + help, + labelNames: labels, + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric): void { + this.calculators.push(calculator) + } + + update (value: number): void { + this.gauge.set(value) + } + + increment (value: number = 1): void { + this.gauge.inc(value) + } + + decrement (value: number = 1): void { + this.gauge.dec(value) + } + + reset (): void { + this.gauge.reset() + } + + timer (): StopTimer { + return this.gauge.startTimer() + } +} diff --git a/packages/metrics-prometheus/src/utils.ts b/packages/metrics-prometheus/src/utils.ts new file mode 100644 index 0000000000..4ec136b846 --- /dev/null +++ b/packages/metrics-prometheus/src/utils.ts @@ -0,0 +1,18 @@ +import type { CalculateMetric } from '@libp2p/interface-metrics' + +export interface CalculatedMetric { + addCalculator: (calculator: CalculateMetric) => void +} + +export const ONE_SECOND = 1000 +export const ONE_MINUTE = 60 * ONE_SECOND + +/** + * See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + * for rules on valid naming + */ +export function normaliseString (str: string): string { + return str + .replace(/[^a-zA-Z0-9_]/g, '_') + .replace(/_+/g, '_') +} diff --git a/packages/metrics-prometheus/test/counter-groups.spec.ts b/packages/metrics-prometheus/test/counter-groups.spec.ts new file mode 100644 index 0000000000..dbb618bd86 --- /dev/null +++ b/packages/metrics-prometheus/test/counter-groups.spec.ts @@ -0,0 +1,120 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('counter groups', () => { + it('should increment a counter group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metrics = prometheusMetrics()() + const metric = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: true + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} 1`, 'did not include updated metric') + }) + + it('should increment a counter group with a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a counter group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounterGroup(metricName, { + label: metricLabel, + calculate: () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a counter group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounterGroup(metricName, { + label: metricLabel, + calculate: async () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a counter group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + + metric.reset() + + await expect(client.register.metrics()).to.eventually.not.include(metricKey, 'still included metric key') + }) + + it('should allow use of the same counter group from multiple reporters', async () => { + const metricName = randomMetricName() + const metricKey1 = randomMetricName('key_') + const metricKey2 = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric1.increment({ + [metricKey1]: metricValue1 + }) + const metric2 = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric2.increment({ + [metricKey2]: metricValue2 + }) + + const reportedMetrics = await client.register.metrics() + + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey1}"} ${metricValue1}`, 'did not include updated metric') + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey2}"} ${metricValue2}`, 'did not include updated metric') + }) +}) diff --git a/packages/metrics-prometheus/test/counters.spec.ts b/packages/metrics-prometheus/test/counters.spec.ts new file mode 100644 index 0000000000..73618430ed --- /dev/null +++ b/packages/metrics-prometheus/test/counters.spec.ts @@ -0,0 +1,85 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('counters', () => { + it('should set a counter', async () => { + const metricName = randomMetricName() + const metrics = prometheusMetrics()() + const metric = metrics.registerCounter(metricName) + metric.increment() + + const report = await client.register.metrics() + expect(report).to.include(`# TYPE ${metricName} counter`, 'did not include metric type') + expect(report).to.include(`${metricName} 1`, 'did not include updated metric') + }) + + it('should increment a counter with a value', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounter(metricName) + metric.increment(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a counter', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounter(metricName, { + calculate: () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a counter', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounter(metricName, { + calculate: async () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a counter', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounter(metricName) + metric.increment(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`) + + metric.reset() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} 0`, 'did not include updated metric') + }) + + it('should allow use of the same counter from multiple reporters', async () => { + const metricName = randomMetricName() + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerCounter(metricName, { + label: metricLabel + }) + metric1.increment(metricValue1) + const metric2 = metrics.registerCounter(metricName, { + label: metricLabel + }) + metric2.increment(metricValue2) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue1 + metricValue2}`) + }) +}) diff --git a/packages/metrics-prometheus/test/custom-registry.spec.ts b/packages/metrics-prometheus/test/custom-registry.spec.ts new file mode 100644 index 0000000000..754179654b --- /dev/null +++ b/packages/metrics-prometheus/test/custom-registry.spec.ts @@ -0,0 +1,23 @@ +import { expect } from 'aegir/chai' +import client, { Registry } from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('custom registry', () => { + it('should set a metric in the custom registry and not in the global registry', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const registry = new Registry() + const metrics = prometheusMetrics({ registry })() + const metric = metrics.registerMetric(metricName) + metric.update(metricValue) + + const customRegistryReport = await registry.metrics() + expect(customRegistryReport).to.include(`# TYPE ${metricName} gauge`, 'did not include metric type') + expect(customRegistryReport).to.include(`${metricName} ${metricValue}`, 'did not include updated metric') + + const globalRegistryReport = await client.register.metrics() + expect(globalRegistryReport).to.not.include(`# TYPE ${metricName} gauge`, 'erroneously includes metric type') + expect(globalRegistryReport).to.not.include(`${metricName} ${metricValue}`, 'erroneously includes updated metric') + }) +}) diff --git a/packages/metrics-prometheus/test/fixtures/random-metric-name.ts b/packages/metrics-prometheus/test/fixtures/random-metric-name.ts new file mode 100644 index 0000000000..be87c5d21b --- /dev/null +++ b/packages/metrics-prometheus/test/fixtures/random-metric-name.ts @@ -0,0 +1,7 @@ +/** + * Prometheus metric names are global and can only contain + * a limited set of, at least /a-z_0-9/i + */ +export function randomMetricName (key = ''): string { + return `my_metric_${key}${Math.random().toString().split('.').pop() ?? ''}` +} diff --git a/packages/metrics-prometheus/test/metric-groups.spec.ts b/packages/metrics-prometheus/test/metric-groups.spec.ts new file mode 100644 index 0000000000..b609da5119 --- /dev/null +++ b/packages/metrics-prometheus/test/metric-groups.spec.ts @@ -0,0 +1,167 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('metric groups', () => { + it('should set a metric group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.update({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should increment a metric group without a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: false + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} 1`, 'did not include updated metric') + }) + + it('should increment a metric group with a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should decrement a metric group without a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.decrement({ + [metricKey]: false + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} -1`, 'did not include updated metric') + }) + + it('should decrement a metric group with a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.decrement({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} -${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a metric group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetricGroup(metricName, { + label: metricLabel, + calculate: () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a metric group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetricGroup(metricName, { + label: metricLabel, + calculate: async () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a metric group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.update({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + + metric.reset() + + await expect(client.register.metrics()).to.eventually.not.include(metricKey, 'still included metric key') + }) + + it('should allow use of the same metric group from multiple reporters', async () => { + const metricName = randomMetricName() + const metricKey1 = randomMetricName('key_') + const metricKey2 = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric1.update({ + [metricKey1]: metricValue1 + }) + const metric2 = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric2.update({ + [metricKey2]: metricValue2 + }) + + const reportedMetrics = await client.register.metrics() + + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey1}"} ${metricValue1}`, 'did not include updated metric') + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey2}"} ${metricValue2}`, 'did not include updated metric') + }) +}) diff --git a/packages/metrics-prometheus/test/metrics.spec.ts b/packages/metrics-prometheus/test/metrics.spec.ts new file mode 100644 index 0000000000..b238cb937e --- /dev/null +++ b/packages/metrics-prometheus/test/metrics.spec.ts @@ -0,0 +1,114 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('metrics', () => { + it('should set a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.update(metricValue) + + const report = await client.register.metrics() + expect(report).to.include(`# TYPE ${metricName} gauge`, 'did not include metric type') + expect(report).to.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should increment a metric without a value', async () => { + const metricName = randomMetricName() + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.increment() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} 1`, 'did not include updated metric') + }) + + it('should increment a metric with a value', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.increment(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should decrement a metric without a value', async () => { + const metricName = randomMetricName() + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.decrement() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} -1`, 'did not include updated metric') + }) + + it('should decrement a metric with a value', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.decrement(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} -${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetric(metricName, { + calculate: () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetric(metricName, { + calculate: async () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.update(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`) + + metric.reset() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} 0`, 'did not include updated metric') + }) + + it('should allow use of the same metric from multiple reporters', async () => { + const metricName = randomMetricName() + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerMetric(metricName, { + label: metricLabel + }) + metric1.update(metricValue1) + const metric2 = metrics.registerMetric(metricName, { + label: metricLabel + }) + metric2.update(metricValue2) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue2}`) + }) +}) diff --git a/packages/metrics-prometheus/test/streams.spec.ts b/packages/metrics-prometheus/test/streams.spec.ts new file mode 100644 index 0000000000..5cfba71e83 --- /dev/null +++ b/packages/metrics-prometheus/test/streams.spec.ts @@ -0,0 +1,167 @@ +import { connectionPair, mockRegistrar, mockMultiaddrConnPair } from '@libp2p/interface-mocks' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import drain from 'it-drain' +import { pipe } from 'it-pipe' +import defer from 'p-defer' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' + +describe('streams', () => { + let connectionA: Connection + let connectionB: Connection + + afterEach(async () => { + if (connectionA != null) { + await connectionA.close() + } + + if (connectionB != null) { + await connectionB.close() + } + }) + + it('should track bytes sent over connections', async () => { + const deferred = defer() + const remotePeer = await createEd25519PeerId() + + const { outbound, inbound } = mockMultiaddrConnPair({ + addrs: [ + multiaddr('/ip4/123.123.123.123/tcp/5923'), + multiaddr('/ip4/123.123.123.123/tcp/5924') + ], + remotePeer + }) + + // process all the bytes + void pipe(inbound, drain).then(() => { + deferred.resolve() + }) + + const metrics = prometheusMetrics()() + + // track outgoing stream + metrics.trackMultiaddrConnection(outbound) + + // send data to the remote over the tracked stream + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await outbound.sink([ + data + ]) + + // wait for all bytes to be received + await deferred.promise + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="global sent"} ${data.length}`) + }) + + it('should track bytes received over connections', async () => { + const deferred = defer() + const remotePeer = await createEd25519PeerId() + + const { outbound, inbound } = mockMultiaddrConnPair({ + addrs: [ + multiaddr('/ip4/123.123.123.123/tcp/5923'), + multiaddr('/ip4/123.123.123.123/tcp/5924') + ], + remotePeer + }) + + const metrics = prometheusMetrics()() + + // track incoming stream + metrics.trackMultiaddrConnection(inbound) + + // send data to the remote over the tracked stream + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await outbound.sink([ + data + ]) + + // process all the bytes + void pipe(inbound, drain).then(() => { + deferred.resolve() + }) + + // wait for all bytes to be received + await deferred.promise + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="global received"} ${data.length}`) + }) + + it('should track sent stream metrics', async () => { + const protocol = '/my-protocol-send/1.0.0' + const peerA = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + const peerB = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + await peerB.registrar.handle(protocol, ({ stream }) => { + void pipe(stream, drain) + }) + + ;[connectionA, connectionB] = connectionPair(peerA, peerB) + const aToB = await connectionA.newStream(protocol) + + const metrics = prometheusMetrics()() + + // track outgoing stream + metrics.trackProtocolStream(aToB, connectionA) + + // send data to the remote over the tracked stream + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await aToB.sink([ + data + ]) + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="${protocol} sent"} ${data.length}`) + }) + + it('should track sent received metrics', async () => { + const deferred = defer() + const protocol = '/my-protocol-receive/1.0.0' + const peerA = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + await peerA.registrar.handle(protocol, ({ stream, connection }) => { + // track incoming stream + metrics.trackProtocolStream(stream, connectionA) + + // ignore data + void pipe(stream, drain).then(() => { + deferred.resolve() + }) + }) + const peerB = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + + const metrics = prometheusMetrics()() + + ;[connectionA, connectionB] = connectionPair(peerA, peerB) + + const bToA = await connectionB.newStream(protocol) + + // send data from remote to local + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await bToA.sink([ + data + ]) + + // wait for data to have been transferred + await deferred.promise + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="${protocol} received"} ${data.length}`) + }) +}) diff --git a/packages/metrics-prometheus/test/utils.spec.ts b/packages/metrics-prometheus/test/utils.spec.ts new file mode 100644 index 0000000000..20da4a9340 --- /dev/null +++ b/packages/metrics-prometheus/test/utils.spec.ts @@ -0,0 +1,12 @@ +import { expect } from 'aegir/chai' +import { normaliseString } from '../src/utils.js' + +describe('utils', () => { + describe('normaliseString', () => { + it('should normalise string', () => { + expect(normaliseString('hello-world')).to.equal('hello_world') + expect(normaliseString('hello---world')).to.equal('hello_world') + expect(normaliseString('hello-world_0.0.0.0:1234-some-metric')).to.equal('hello_world_0_0_0_0_1234_some_metric') + }) + }) +}) diff --git a/packages/metrics-prometheus/tsconfig.json b/packages/metrics-prometheus/tsconfig.json new file mode 100644 index 0000000000..20f43dc904 --- /dev/null +++ b/packages/metrics-prometheus/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/multistream-select/CHANGELOG.md b/packages/multistream-select/CHANGELOG.md new file mode 100644 index 0000000000..2b8fdbdeaf --- /dev/null +++ b/packages/multistream-select/CHANGELOG.md @@ -0,0 +1,200 @@ +## [3.1.9](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.8...v3.1.9) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([c03b6cd](https://github.com/libp2p/js-libp2p-multistream-select/commit/c03b6cd8013a82605f414a5ddbde7c66c84e4db1)) +* Update .github/workflows/stale.yml [skip ci] ([e8d5014](https://github.com/libp2p/js-libp2p-multistream-select/commit/e8d5014b6da7bf4db1cc542c5d923760a6067903)) + + +### Dependencies + +* **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)) + +## [3.1.8](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.7...v3.1.8) (2023-04-19) + + +### Dependencies + +* update abortable iterator to 5.x.x ([#61](https://github.com/libp2p/js-libp2p-multistream-select/issues/61)) ([5bc4293](https://github.com/libp2p/js-libp2p-multistream-select/commit/5bc42936e25e14791d19fdd790d3c3987c56e784)) + +## [3.1.7](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.6...v3.1.7) (2023-04-18) + + +### Bug Fixes + +* specify protocol stream sink return type ([#60](https://github.com/libp2p/js-libp2p-multistream-select/issues/60)) ([12d6b9c](https://github.com/libp2p/js-libp2p-multistream-select/commit/12d6b9c4ea26b26d0428df2d05c3078464068392)) + +## [3.1.6](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.5...v3.1.6) (2023-04-18) + + +### Dependencies + +* bump it-stream-types from 1.0.5 to 2.0.1 ([#58](https://github.com/libp2p/js-libp2p-multistream-select/issues/58)) ([0b0ebca](https://github.com/libp2p/js-libp2p-multistream-select/commit/0b0ebcadd0ccbbfd373ebbf8e9fb5a8b793fc924)) + +## [3.1.5](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.4...v3.1.5) (2023-04-17) + + +### Bug Fixes + +* use trace logging for happy paths ([#59](https://github.com/libp2p/js-libp2p-multistream-select/issues/59)) ([184ef21](https://github.com/libp2p/js-libp2p-multistream-select/commit/184ef21c930c1557d657ce0891471d86f76fb271)) + +## [3.1.4](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.3...v3.1.4) (2023-04-03) + + +### Dependencies + +* update all it-* deps to the latest versions ([#57](https://github.com/libp2p/js-libp2p-multistream-select/issues/57)) ([cf9133a](https://github.com/libp2p/js-libp2p-multistream-select/commit/cf9133a00b73c9e6d7576b57d2dccd9e87ccd01e)) + +## [3.1.3](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.2...v3.1.3) (2023-03-31) + + +### Trivial Changes + +* replace err-code with CodeError ([#36](https://github.com/libp2p/js-libp2p-multistream-select/issues/36)) ([fc2aefd](https://github.com/libp2p/js-libp2p-multistream-select/commit/fc2aefdec0db9a2b39fe8259881cf3a2693027cb)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1861a94](https://github.com/libp2p/js-libp2p-multistream-select/commit/1861a945fd8fef3d407591632d92f080d07e0bed)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([0f312c0](https://github.com/libp2p/js-libp2p-multistream-select/commit/0f312c08f3760f188304074088060f3d701e5815)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6a277a6](https://github.com/libp2p/js-libp2p-multistream-select/commit/6a277a6efdcbd3afef72335699d3a61e4bbea609)) + + +### Dependencies + +* bump it-merge from 2.0.1 to 3.0.0 ([#51](https://github.com/libp2p/js-libp2p-multistream-select/issues/51)) ([129166b](https://github.com/libp2p/js-libp2p-multistream-select/commit/129166ba5366d29d20e2629ce1f542c57cc864ba)) +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#50](https://github.com/libp2p/js-libp2p-multistream-select/issues/50)) ([d8420a0](https://github.com/libp2p/js-libp2p-multistream-select/commit/d8420a03207be7ee3472c4bb7a4f3cc0912758a1)) + +## [3.1.2](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.1...v3.1.2) (2022-12-16) + + +### Trivial Changes + +* log invalid buffer ([#30](https://github.com/libp2p/js-libp2p-multistream-select/issues/30)) ([1fce957](https://github.com/libp2p/js-libp2p-multistream-select/commit/1fce9579eefe32a81b9805edc6a348f37605ac7f)) +* update it-* deps ([#31](https://github.com/libp2p/js-libp2p-multistream-select/issues/31)) ([3caf904](https://github.com/libp2p/js-libp2p-multistream-select/commit/3caf904c20aab7dc4ca61f40420b18e84bbd2c49)) + + +### Documentation + +* publish api docs ([#35](https://github.com/libp2p/js-libp2p-multistream-select/issues/35)) ([c4c978a](https://github.com/libp2p/js-libp2p-multistream-select/commit/c4c978ac1eb84667d5568c5f68a6678cf460380f)) + +## [3.1.1](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.0...v3.1.1) (2022-10-31) + + +### Bug Fixes + +* set min and max protocol length ([#21](https://github.com/libp2p/js-libp2p-multistream-select/issues/21)) ([ae42f76](https://github.com/libp2p/js-libp2p-multistream-select/commit/ae42f7623b557d33208c12c69d7f01e49f478fdb)) + + +### Trivial Changes + +* update to handshake 4.1.2 ([#28](https://github.com/libp2p/js-libp2p-multistream-select/issues/28)) ([53883b1](https://github.com/libp2p/js-libp2p-multistream-select/commit/53883b1c6215584043f4dd6e97e2d10adb890af6)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.0.0...v3.1.0) (2022-10-12) + + +### Features + +* add lazy select ([#18](https://github.com/libp2p/js-libp2p-multistream-select/issues/18)) ([d3bff7c](https://github.com/libp2p/js-libp2p-multistream-select/commit/d3bff7cc3cd5afe6ebc1355241030868ec0aa572)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([ba9ea12](https://github.com/libp2p/js-libp2p-multistream-select/commit/ba9ea12b2b55602bbeb6c9227976419851496783)) + + +### Dependencies + +* bump uint8arrays from 3.x.x to 4.x.x ([#22](https://github.com/libp2p/js-libp2p-multistream-select/issues/22)) ([cfb887b](https://github.com/libp2p/js-libp2p-multistream-select/commit/cfb887b9bc01f8234838049c59866064db97bdf5)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-multistream-select/compare/v2.0.2...v3.0.0) (2022-08-06) + + +### ⚠ BREAKING CHANGES + +* the single-method Listener and Dialer classes have been removed and their methods exported instead + +### Bug Fixes + +* support Duplex and Duplex ([#17](https://github.com/libp2p/js-libp2p-multistream-select/issues/17)) ([6e96c89](https://github.com/libp2p/js-libp2p-multistream-select/commit/6e96c89b68a77ea5192e91cab5547e78f5b078fd)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-multistream-select/compare/v2.0.1...v2.0.2) (2022-07-31) + + +### Trivial Changes + +* update project config ([#14](https://github.com/libp2p/js-libp2p-multistream-select/issues/14)) ([4d4ef28](https://github.com/libp2p/js-libp2p-multistream-select/commit/4d4ef28af8cb8d0f57e06d9ae161ba31e2c5e814)) + + +### Dependencies + +* update it-length-prefixed deps to support no-copy ops ([#16](https://github.com/libp2p/js-libp2p-multistream-select/issues/16)) ([2946064](https://github.com/libp2p/js-libp2p-multistream-select/commit/2946064a8993b4ec70ebfd3e5a34d86db1ee7fe6)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-multistream-select/compare/v2.0.0...v2.0.1) (2022-06-17) + + +### Trivial Changes + +* update deps ([#9](https://github.com/libp2p/js-libp2p-multistream-select/issues/9)) ([dc5ddc1](https://github.com/libp2p/js-libp2p-multistream-select/commit/dc5ddc1b93da82a98e5acddc25a8e41c6eb67044)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-multistream-select/compare/v1.0.6...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* updates to single-issue libp2p interfaces and ls has been removed + +### Features + +* update interfaces, remove ls ([#3](https://github.com/libp2p/js-libp2p-multistream-select/issues/3)) ([1e6f3cd](https://github.com/libp2p/js-libp2p-multistream-select/commit/1e6f3cdffee6683786349142349a50872fa8fd17)), closes [#2](https://github.com/libp2p/js-libp2p-multistream-select/issues/2) + +## [@libp2p/multistream-select-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.5...@libp2p/multistream-select-v1.0.6) (2022-05-24) + + +### Bug Fixes + +* chunk data in mock muxer ([#218](https://github.com/libp2p/js-libp2p-interfaces/issues/218)) ([14604f6](https://github.com/libp2p/js-libp2p-interfaces/commit/14604f69a858bf8c16ce118420c5e49f3f5331ea)) + +## [@libp2p/multistream-select-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.4...@libp2p/multistream-select-v1.0.5) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/multistream-select-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.3...@libp2p/multistream-select-v1.0.4) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/multistream-select-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.2...@libp2p/multistream-select-v1.0.3) (2022-03-15) + + +### 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)) + +## [@libp2p/multistream-select-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.1...@libp2p/multistream-select-v1.0.2) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/multistream-select-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.0...@libp2p/multistream-select-v1.0.1) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## @libp2p/multistream-select-v1.0.0 (2022-02-17) + + +### 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)) diff --git a/packages/multistream-select/LICENSE b/packages/multistream-select/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/multistream-select/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/multistream-select/LICENSE-APACHE b/packages/multistream-select/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/multistream-select/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/multistream-select/LICENSE-MIT b/packages/multistream-select/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/multistream-select/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/multistream-select/README.md b/packages/multistream-select/README.md new file mode 100644 index 0000000000..e28849ffbe --- /dev/null +++ b/packages/multistream-select/README.md @@ -0,0 +1,68 @@ +# @libp2p/multistream-select + +[![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 multistream-select + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Background + +### What is `multistream-select`? + +TLDR; multistream-select is protocol multiplexing per connection/stream. [Full spec here](https://github.com/multiformats/multistream-select) + +### Select a protocol flow + +The caller will send "interactive" messages, expecting for some acknowledgement from the callee, which will "select" the handler for the desired and supported protocol: + + < /multistream-select/0.3.0 # i speak multistream-select/0.3.0 + > /multistream-select/0.3.0 # ok, let's speak multistream-select/0.3.0 + > /ipfs-dht/0.2.3 # i want to speak ipfs-dht/0.2.3 + < na # ipfs-dht/0.2.3 is not available + > /ipfs-dht/0.1.9 # What about ipfs-dht/0.1.9 ? + < /ipfs-dht/0.1.9 # ok let's speak ipfs-dht/0.1.9 -- in a sense acts as an ACK + > + > + > + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/multistream-select/package.json b/packages/multistream-select/package.json new file mode 100644 index 0000000000..156907a437 --- /dev/null +++ b/packages/multistream-select/package.json @@ -0,0 +1,82 @@ +{ + "name": "@libp2p/multistream-select", + "version": "3.1.9", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "ipfs", + "libp2p", + "multistream", + "protocol", + "stream" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "abortable-iterator": "^5.0.1", + "it-first": "^3.0.1", + "it-handshake": "^4.1.3", + "it-length-prefixed": "^9.0.1", + "it-merge": "^3.0.0", + "it-pipe": "^3.0.1", + "it-pushable": "^3.1.3", + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@types/varint": "^6.0.0", + "aegir": "^39.0.10", + "iso-random-stream": "^2.0.2", + "it-all": "^3.0.1", + "it-map": "^3.0.3", + "it-pair": "^2.0.6", + "p-timeout": "^6.0.0", + "varint": "^6.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/multistream-select/src/constants.ts b/packages/multistream-select/src/constants.ts new file mode 100644 index 0000000000..ce5dbb521e --- /dev/null +++ b/packages/multistream-select/src/constants.ts @@ -0,0 +1,6 @@ + +export const PROTOCOL_ID = '/multistream/1.0.0' + +// Conforming to go-libp2p +// See https://github.com/multiformats/go-multistream/blob/master/multistream.go#L297 +export const MAX_PROTOCOL_LENGTH = 1024 diff --git a/packages/multistream-select/src/handle.ts b/packages/multistream-select/src/handle.ts new file mode 100644 index 0000000000..eaf8331f6b --- /dev/null +++ b/packages/multistream-select/src/handle.ts @@ -0,0 +1,92 @@ +import { logger } from '@libp2p/logger' +import { handshake } from 'it-handshake' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PROTOCOL_ID } from './constants.js' +import * as multistream from './multistream.js' +import type { ByteArrayInit, ByteListInit, MultistreamSelectInit, ProtocolStream } from './index.js' +import type { Duplex, Source } from 'it-stream-types' + +const log = logger('libp2p:mss:handle') + +/** + * Handle multistream protocol selections for the given list of protocols. + * + * Note that after a protocol is handled `listener` can no longer be used. + * + * @param stream - A duplex iterable stream to listen on + * @param protocols - A list of protocols (or single protocol) that this listener is able to speak. + * @param options - an options object containing an AbortSignal and an optional boolean `writeBytes` - if this is true, `Uint8Array`s will be written into `duplex`, otherwise `Uint8ArrayList`s will + * @returns A stream for the selected protocol and the protocol that was selected from the list of protocols provided to `select` + * @example + * + * ```js + * import { pipe } from 'it-pipe' + * import * as mss from '@libp2p/multistream-select' + * import { Mplex } from '@libp2p/mplex' + * + * const muxer = new Mplex({ + * async onStream (muxedStream) { + * // mss.handle(handledProtocols) + * // Returns selected stream and protocol + * const { stream, protocol } = await mss.handle(muxedStream, [ + * '/ipfs-dht/1.0.0', + * '/ipfs-bitswap/1.0.0' + * ]) + * + * // Typically here we'd call the handler function that was registered in + * // libp2p for the given protocol: + * // e.g. handlers[protocol].handler(stream) + * // + * // If protocol was /ipfs-dht/1.0.0 it might do something like this: + * // try { + * // await pipe( + * // dhtStream, + * // source => (async function * () { + * // for await (const chunk of source) + * // // Incoming DHT data -> process and yield to respond + * // })(), + * // dhtStream + * // ) + * // } catch (err) { + * // // Error in stream + * // } + * } + * }) + * ``` + */ +export async function handle (stream: Duplex, Source>, protocols: string | string[], options: ByteArrayInit): Promise> +export async function handle (stream: Duplex, Source>, protocols: string | string[], options?: ByteListInit): Promise> +export async function handle (stream: any, protocols: string | string[], options?: MultistreamSelectInit): Promise> { + protocols = Array.isArray(protocols) ? protocols : [protocols] + const { writer, reader, rest, stream: shakeStream } = handshake(stream) + + while (true) { + const protocol = await multistream.readString(reader, options) + log.trace('read "%s"', protocol) + + if (protocol === PROTOCOL_ID) { + log.trace('respond with "%s" for "%s"', PROTOCOL_ID, protocol) + multistream.write(writer, uint8ArrayFromString(PROTOCOL_ID), options) + continue + } + + if (protocols.includes(protocol)) { + multistream.write(writer, uint8ArrayFromString(protocol), options) + log.trace('respond with "%s" for "%s"', protocol, protocol) + rest() + return { stream: shakeStream, protocol } + } + + if (protocol === 'ls') { + // \n\n\n + multistream.write(writer, new Uint8ArrayList(...protocols.map(p => multistream.encode(uint8ArrayFromString(p)))), options) + // multistream.writeAll(writer, protocols.map(p => uint8ArrayFromString(p))) + log.trace('respond with "%s" for %s', protocols, protocol) + continue + } + + multistream.write(writer, uint8ArrayFromString('na'), options) + log('respond with "na" for "%s"', protocol) + } +} diff --git a/packages/multistream-select/src/index.ts b/packages/multistream-select/src/index.ts new file mode 100644 index 0000000000..651fd77cce --- /dev/null +++ b/packages/multistream-select/src/index.ts @@ -0,0 +1,25 @@ +import { PROTOCOL_ID } from './constants.js' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Duplex, Source } from 'it-stream-types' + +export { PROTOCOL_ID } + +export interface ProtocolStream> { + stream: Duplex, Source, RSink> + protocol: string +} + +export interface ByteArrayInit extends AbortOptions { + writeBytes: true +} + +export interface ByteListInit extends AbortOptions { + writeBytes?: false +} + +export interface MultistreamSelectInit extends AbortOptions { + writeBytes?: boolean +} + +export { select, lazySelect } from './select.js' +export { handle } from './handle.js' diff --git a/packages/multistream-select/src/multistream.ts b/packages/multistream-select/src/multistream.ts new file mode 100644 index 0000000000..65eda32839 --- /dev/null +++ b/packages/multistream-select/src/multistream.ts @@ -0,0 +1,100 @@ + +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import first from 'it-first' +import * as lp from 'it-length-prefixed' +import { pipe } from 'it-pipe' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { MAX_PROTOCOL_LENGTH } from './constants.js' +import type { MultistreamSelectInit } from '.' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Pushable } from 'it-pushable' +import type { Reader } from 'it-reader' +import type { Source } from 'it-stream-types' + +const log = logger('libp2p:mss') + +const NewLine = uint8ArrayFromString('\n') + +export function encode (buffer: Uint8Array | Uint8ArrayList): Uint8ArrayList { + const list = new Uint8ArrayList(buffer, NewLine) + + return lp.encode.single(list) +} + +/** + * `write` encodes and writes a single buffer + */ +export function write (writer: Pushable, buffer: Uint8Array | Uint8ArrayList, options: MultistreamSelectInit = {}): void { + const encoded = encode(buffer) + + if (options.writeBytes === true) { + writer.push(encoded.subarray()) + } else { + writer.push(encoded) + } +} + +/** + * `writeAll` behaves like `write`, except it encodes an array of items as a single write + */ +export function writeAll (writer: Pushable, buffers: Uint8Array[], options: MultistreamSelectInit = {}): void { + const list = new Uint8ArrayList() + + for (const buf of buffers) { + list.append(encode(buf)) + } + + if (options.writeBytes === true) { + writer.push(list.subarray()) + } else { + writer.push(list) + } +} + +export async function read (reader: Reader, options?: AbortOptions): Promise { + let byteLength = 1 // Read single byte chunks until the length is known + const varByteSource = { // No return impl - we want the reader to remain readable + [Symbol.asyncIterator]: () => varByteSource, + next: async () => reader.next(byteLength) + } + + let input: Source = varByteSource + + // If we have been passed an abort signal, wrap the input source in an abortable + // iterator that will throw if the operation is aborted + if (options?.signal != null) { + input = abortableSource(varByteSource, options.signal) + } + + // Once the length has been parsed, read chunk for that length + const onLength = (l: number): void => { + byteLength = l + } + + const buf = await pipe( + input, + (source) => lp.decode(source, { onLength, maxDataLength: MAX_PROTOCOL_LENGTH }), + async (source) => first(source) + ) + + if (buf == null || buf.length === 0) { + throw new CodeError('no buffer returned', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + } + + if (buf.get(buf.byteLength - 1) !== NewLine[0]) { + log.error('Invalid mss message - missing newline - %s', buf.subarray()) + throw new CodeError('missing newline', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + } + + return buf.sublist(0, -1) // Remove newline +} + +export async function readString (reader: Reader, options?: AbortOptions): Promise { + const buf = await read(reader, options) + + return uint8ArrayToString(buf.subarray()) +} diff --git a/packages/multistream-select/src/select.ts b/packages/multistream-select/src/select.ts new file mode 100644 index 0000000000..71dafed647 --- /dev/null +++ b/packages/multistream-select/src/select.ts @@ -0,0 +1,161 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { handshake } from 'it-handshake' +import merge from 'it-merge' +import { pushable } from 'it-pushable' +import { reader } from 'it-reader' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as multistream from './multistream.js' +import { PROTOCOL_ID } from './index.js' +import type { ByteArrayInit, ByteListInit, MultistreamSelectInit, ProtocolStream } from './index.js' +import type { Duplex, Source } from 'it-stream-types' + +const log = logger('libp2p:mss:select') + +/** + * Negotiate a protocol to use from a list of protocols. + * + * @param stream - A duplex iterable stream to dial on + * @param protocols - A list of protocols (or single protocol) to negotiate with. Protocols are attempted in order until a match is made. + * @param options - An options object containing an AbortSignal and an optional boolean `writeBytes` - if this is true, `Uint8Array`s will be written into `duplex`, otherwise `Uint8ArrayList`s will + * @returns A stream for the selected protocol and the protocol that was selected from the list of protocols provided to `select`. + * @example + * + * ```js + * import { pipe } from 'it-pipe' + * import * as mss from '@libp2p/multistream-select' + * import { Mplex } from '@libp2p/mplex' + * + * const muxer = new Mplex() + * const muxedStream = muxer.newStream() + * + * // mss.select(protocol(s)) + * // Select from one of the passed protocols (in priority order) + * // Returns selected stream and protocol + * const { stream: dhtStream, protocol } = await mss.select(muxedStream, [ + * // This might just be different versions of DHT, but could be different impls + * '/ipfs-dht/2.0.0', // Most of the time this will probably just be one item. + * '/ipfs-dht/1.0.0' + * ]) + * + * // Typically this stream will be passed back to the caller of libp2p.dialProtocol + * // + * // ...it might then do something like this: + * // try { + * // await pipe( + * // [uint8ArrayFromString('Some DHT data')] + * // dhtStream, + * // async source => { + * // for await (const chunk of source) + * // // DHT response data + * // } + * // ) + * // } catch (err) { + * // // Error in stream + * // } + * ``` + */ +export async function select (stream: Duplex, Source>, protocols: string | string[], options: ByteArrayInit): Promise> +export async function select (stream: Duplex, Source>, protocols: string | string[], options?: ByteListInit): Promise> +export async function select (stream: any, protocols: string | string[], options: MultistreamSelectInit = {}): Promise> { + protocols = Array.isArray(protocols) ? [...protocols] : [protocols] + const { reader, writer, rest, stream: shakeStream } = handshake(stream) + + const protocol = protocols.shift() + + if (protocol == null) { + throw new Error('At least one protocol must be specified') + } + + log.trace('select: write ["%s", "%s"]', PROTOCOL_ID, protocol) + const p1 = uint8ArrayFromString(PROTOCOL_ID) + const p2 = uint8ArrayFromString(protocol) + multistream.writeAll(writer, [p1, p2], options) + + let response = await multistream.readString(reader, options) + log.trace('select: read "%s"', response) + + // Read the protocol response if we got the protocolId in return + if (response === PROTOCOL_ID) { + response = await multistream.readString(reader, options) + log.trace('select: read "%s"', response) + } + + // We're done + if (response === protocol) { + rest() + return { stream: shakeStream, protocol } + } + + // We haven't gotten a valid ack, try the other protocols + for (const protocol of protocols) { + log.trace('select: write "%s"', protocol) + multistream.write(writer, uint8ArrayFromString(protocol), options) + const response = await multistream.readString(reader, options) + log.trace('select: read "%s" for "%s"', response, protocol) + + if (response === protocol) { + rest() // End our writer so others can start writing to stream + return { stream: shakeStream, protocol } + } + } + + rest() + throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL') +} + +/** + * Lazily negotiates a protocol. + * + * It *does not* block writes waiting for the other end to respond. Instead, it + * simply assumes the negotiation went successfully and starts writing data. + * + * Use when it is known that the receiver supports the desired protocol. + */ +export function lazySelect (stream: Duplex, Source>, protocol: string): ProtocolStream +export function lazySelect (stream: Duplex, Source>, protocol: string): ProtocolStream +export function lazySelect (stream: Duplex, protocol: string): ProtocolStream { + // This is a signal to write the multistream headers if the consumer tries to + // read from the source + const negotiateTrigger = pushable() + let negotiated = false + return { + stream: { + sink: async source => { + await stream.sink((async function * () { + let first = true + for await (const chunk of merge(source, negotiateTrigger)) { + if (first) { + first = false + negotiated = true + negotiateTrigger.end() + const p1 = uint8ArrayFromString(PROTOCOL_ID) + const p2 = uint8ArrayFromString(protocol) + const list = new Uint8ArrayList(multistream.encode(p1), multistream.encode(p2)) + if (chunk.length > 0) list.append(chunk) + yield * list + } else { + yield chunk + } + } + })()) + }, + source: (async function * () { + if (!negotiated) negotiateTrigger.push(new Uint8Array()) + const byteReader = reader(stream.source) + let response = await multistream.readString(byteReader) + if (response === PROTOCOL_ID) { + response = await multistream.readString(byteReader) + } + if (response !== protocol) { + throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL') + } + for await (const chunk of byteReader) { + yield * chunk + } + })() + }, + protocol + } +} diff --git a/packages/multistream-select/test/dialer.spec.ts b/packages/multistream-select/test/dialer.spec.ts new file mode 100644 index 0000000000..fe77684dfb --- /dev/null +++ b/packages/multistream-select/test/dialer.spec.ts @@ -0,0 +1,139 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import { pair } from 'it-pair' +import { pipe } from 'it-pipe' +import { reader } from 'it-reader' +import pTimeout from 'p-timeout' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as mss from '../src/index.js' +import * as Multistream from '../src/multistream.js' + +describe('Dialer', () => { + describe('dialer.select', () => { + it('should select from single protocol', async () => { + const protocol = '/echo/1.0.0' + const duplex = pair() + + const selection = await mss.select(duplex, protocol, { + writeBytes: true + }) + expect(selection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [randomBytes(10), randomBytes(64), randomBytes(3)] + const output = await pipe(input, selection.stream, async (source) => all(source)) + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should fail to select twice', async () => { + const protocol = '/echo/1.0.0' + const protocol2 = '/echo/2.0.0' + const duplex = pair() + + const selection = await mss.select(duplex, protocol, { + writeBytes: true + }) + expect(selection.protocol).to.equal(protocol) + + // A second select will timeout + await pTimeout(mss.select(duplex, protocol2, { + writeBytes: true + }), { + milliseconds: 1e3 + }) + .then(() => expect.fail('should have timed out'), (err) => { + expect(err).to.exist() + }) + }) + + it('should select from multiple protocols', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const stream = pair() + const duplex = { + sink: stream.sink, + source: (async function * () { + const source = reader(stream.source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(source) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Echo it back + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + + // Reject protocols until selectedProtocol appears + while (true) { + msg = await Multistream.readString(source) + if (msg === selectedProtocol) { + yield Multistream.encode(uint8ArrayFromString(selectedProtocol)) + break + } else { + yield Multistream.encode(uint8ArrayFromString('na')) + } + } + + // Rest is data + yield * source + })() + } + + const selection = await mss.select(duplex, protocols) + expect(protocols).to.have.length(2) + expect(selection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + const output = await pipe(input, selection.stream, async (source) => all(source)) + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should throw if protocol selection fails', async () => { + const protocol = ['/echo/2.0.0', '/echo/1.0.0'] + const stream = pair() + const duplex = { + sink: stream.sink, + source: (async function * () { + const source = reader(stream.source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(source) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Echo it back + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + + // Reject all protocols + while (true) { + msg = await Multistream.readString(source) + yield Multistream.encode(uint8ArrayFromString('na')) + } + })() + } + + await expect(mss.select(duplex, protocol)).to.eventually.be.rejected().with.property('code', 'ERR_UNSUPPORTED_PROTOCOL') + }) + }) + + describe('dialer.lazySelect', () => { + it('should lazily select a single protocol', async () => { + const protocol = '/echo/1.0.0' + const duplex = pair() + + const selection = mss.lazySelect(duplex, protocol) + expect(selection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [randomBytes(10), randomBytes(64), randomBytes(3)] + const output = await pipe(input, selection.stream, async (source) => all(source)) + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + }) +}) diff --git a/packages/multistream-select/test/integration.spec.ts b/packages/multistream-select/test/integration.spec.ts new file mode 100644 index 0000000000..e455e02ae1 --- /dev/null +++ b/packages/multistream-select/test/integration.spec.ts @@ -0,0 +1,120 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import { duplexPair } from 'it-pair/duplex' +import { pipe } from 'it-pipe' +import { Uint8ArrayList } from 'uint8arraylist' +import * as mss from '../src/index.js' + +describe('Dialer and Listener integration', () => { + it('should handle and select', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const pair = duplexPair() + + const [dialerSelection, listenerSelection] = await Promise.all([ + mss.select(pair[0], protocols), + mss.handle(pair[1], selectedProtocol) + ]) + + expect(dialerSelection.protocol).to.equal(selectedProtocol) + expect(listenerSelection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + const output = await Promise.all([ + pipe(input, dialerSelection.stream, async (source) => all(source)), + pipe(listenerSelection.stream, listenerSelection.stream) + ]) + expect(new Uint8ArrayList(...output[0]).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle, ls and select', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const pair = duplexPair() + + const [listenerSelection, dialerSelection] = await Promise.all([ + mss.handle(pair[1], selectedProtocol), + (async () => mss.select(pair[0], selectedProtocol))() + ]) + + expect(dialerSelection.protocol).to.equal(selectedProtocol) + expect(listenerSelection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + const output = await Promise.all([ + pipe(input, dialerSelection.stream, async (source) => all(source)), + pipe(listenerSelection.stream, listenerSelection.stream) + ]) + expect(new Uint8ArrayList(...output[0]).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle and select with Uint8Array streams', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const pair = duplexPair() + + const [dialerSelection, listenerSelection] = await Promise.all([ + mss.select(pair[0], protocols), + mss.handle(pair[1], selectedProtocol) + ]) + + expect(dialerSelection.protocol).to.equal(selectedProtocol) + expect(listenerSelection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [randomBytes(10), randomBytes(64), randomBytes(3)] + const output = await Promise.all([ + pipe(input, dialerSelection.stream, async (source) => all(source)), + pipe(listenerSelection.stream, listenerSelection.stream) + ]) + expect(new Uint8ArrayList(...output[0]).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle and lazySelect', async () => { + const protocol = '/echo/1.0.0' + const pair = duplexPair() + + const dialerSelection = mss.lazySelect(pair[0], protocol) + expect(dialerSelection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + // Since the stream is lazy, we need to write to it before handling + const dialerOutPromise = pipe(input, dialerSelection.stream, async source => all(source)) + + const listenerSelection = await mss.handle(pair[1], protocol) + expect(listenerSelection.protocol).to.equal(protocol) + + await pipe(listenerSelection.stream, listenerSelection.stream) + + const dialerOut = await dialerOutPromise + expect(new Uint8ArrayList(...dialerOut).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should abort an unhandled lazySelect', async () => { + const protocol = '/echo/1.0.0' + const pair = duplexPair() + + const dialerSelection = mss.lazySelect(pair[0], protocol) + expect(dialerSelection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + // Since the stream is lazy, we need to write to it before handling + const dialerResultPromise = pipe(input, dialerSelection.stream, async source => all(source)) + + // The error message from this varies depending on how much data got + // written when the dialer receives the `na` response and closes the + // stream, so we just assert that this rejects. + await expect(mss.handle(pair[1], '/unhandled/1.0.0')).to.eventually.be.rejected() + + // Dialer should fail to negotiate the single protocol + await expect(dialerResultPromise).to.eventually.be.rejected() + .with.property('code', 'ERR_UNSUPPORTED_PROTOCOL') + }) +}) diff --git a/packages/multistream-select/test/listener.spec.ts b/packages/multistream-select/test/listener.spec.ts new file mode 100644 index 0000000000..eb9a8b9f40 --- /dev/null +++ b/packages/multistream-select/test/listener.spec.ts @@ -0,0 +1,154 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import * as Lp from 'it-length-prefixed' +import map from 'it-map' +import { pipe } from 'it-pipe' +import { reader } from 'it-reader' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import * as mss from '../src/index.js' +import * as Multistream from '../src/multistream.js' +import type { Duplex, Source } from 'it-stream-types' + +describe('Listener', () => { + describe('listener.handle', () => { + it('should handle a protocol', async () => { + const protocol = '/echo/1.0.0' + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + let output: Uint8ArrayList[] = [] + + const duplex: Duplex, Source> = { + sink: async source => { + const read = reader(source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(read) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Second message will be protocol + msg = await Multistream.readString(read) + expect(msg).to.equal(protocol) + + // Rest is data + output = await all(read) + }, + source: (async function * () { + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + yield Multistream.encode(uint8ArrayFromString(protocol)) + yield * input + })() + } + + const selection = await mss.handle(duplex, protocol) + expect(selection.protocol).to.equal(protocol) + + await pipe(selection.stream, selection.stream) + + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should reject unhandled protocols', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const handledProtocols = ['/test/1.0.0', protocols[protocols.length - 1]] + const handledProtocol = protocols[protocols.length - 1] + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + let output: Uint8ArrayList[] = [] + + const duplex: Duplex, Source> = { + sink: async source => { + const read = reader(source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(read) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Second message will be na + msg = await Multistream.readString(read) + expect(msg).to.equal('na') + + // Third message will be handledProtocol + msg = await Multistream.readString(read) + expect(msg).to.equal(handledProtocol) + + // Rest is data + output = await all(read) + }, + source: (function * () { + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + for (const protocol of protocols) { + yield Multistream.encode(uint8ArrayFromString(protocol)) + } + yield * input + })() + } + + const selection = await mss.handle(duplex, handledProtocols) + expect(selection.protocol).to.equal(handledProtocol) + + await pipe(selection.stream, selection.stream) + + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle ls', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const handledProtocols = ['/test/1.0.0', protocols[protocols.length - 1]] + const handledProtocol = protocols[protocols.length - 1] + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + let output: Uint8ArrayList[] = [] + + const duplex: Duplex, Source> = { + sink: async source => { + const read = reader(source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(read) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Second message will be ls response + const buf = await Multistream.read(read) + + const protocolsReader = reader([buf]) + + // Decode each of the protocols from the reader + const lsProtocols = await pipe( + protocolsReader, + (source) => Lp.decode(source), + // Stringify and remove the newline + (source) => map(source, (buf) => uint8ArrayToString(buf.subarray()).trim()), + async (source) => all(source) + ) + + expect(lsProtocols).to.deep.equal(handledProtocols) + + // Third message will be handledProtocol + msg = await Multistream.readString(read) + expect(msg).to.equal(handledProtocol) + + // Rest is data + output = await all(read) + }, + source: (function * () { + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + yield Multistream.encode(uint8ArrayFromString('ls')) + yield Multistream.encode(uint8ArrayFromString(handledProtocol)) + yield * input + })() + } + + const selection = await mss.handle(duplex, handledProtocols) + expect(selection.protocol).to.equal(handledProtocol) + + await pipe(selection.stream, selection.stream) + + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + }) +}) diff --git a/packages/multistream-select/test/multistream.spec.ts b/packages/multistream-select/test/multistream.spec.ts new file mode 100644 index 0000000000..e02d88b9f8 --- /dev/null +++ b/packages/multistream-select/test/multistream.spec.ts @@ -0,0 +1,132 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pushable } from 'it-pushable' +import { reader } from 'it-reader' +import { Uint8ArrayList } from 'uint8arraylist' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as Varint from 'varint' +import * as Multistream from '../src/multistream.js' + +describe('Multistream', () => { + describe('Multistream.encode', () => { + it('should encode data Buffer as a multistream-select message', () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + const output = Multistream.encode(input) + + const expected = uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ]) + + expect(output.slice()).to.eql(expected) + }) + + it('should encode data Uint8ArrayList as a multistream-select message', () => { + const input = new Uint8ArrayList(uint8ArrayFromString('TEST'), uint8ArrayFromString(`${Date.now()}`)) + const output = Multistream.encode(input.slice()) + + const expected = uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input.slice(), + uint8ArrayFromString('\n') + ]) + + expect(output.slice()).to.eql(expected) + }) + }) + + describe('Multistream.write', () => { + it('should encode and write a multistream-select message', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + const writer = pushable() + + Multistream.write(writer, input) + + const expected = uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ]) + + writer.end() + + const output = await all(writer) + expect(output.length).to.equal(1) + expect(output[0].subarray()).to.equalBytes(expected) + }) + }) + + describe('Multistream.read', () => { + it('should decode a multistream-select message', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ])]) + + const output = await Multistream.read(source) + expect(output.subarray()).to.equalBytes(input) + }) + + it('should throw for non-newline delimited message', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length)), + input + ])]) + + await expect(Multistream.read(source)).to.eventually.be.rejected() + .with.property('code', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + }) + + it('should throw for a large message', async () => { + const input = new Uint8Array(10000) + input[input.length - 1] = '\n'.charCodeAt(0) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length)), + input + ])]) + + await expect(Multistream.read(source)).to.eventually.be.rejected() + .with.property('code', 'ERR_MSG_DATA_TOO_LONG') + }) + + it('should throw for a 0-length message', async () => { + const input = new Uint8Array(0) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length)), + input + ])]) + + await expect(Multistream.read(source)).to.eventually.be.rejected() + .with.property('code', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + }) + + it('should be abortable', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ])]) + + const controller = new AbortController() + controller.abort() + + await expect(Multistream.read(source, { + signal: controller.signal + })).to.eventually.be.rejected().with.property('code', 'ABORT_ERR') + }) + }) +}) diff --git a/packages/multistream-select/tsconfig.json b/packages/multistream-select/tsconfig.json new file mode 100644 index 0000000000..4f2b857b3b --- /dev/null +++ b/packages/multistream-select/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interfaces" + }, + { + "path": "../logger" + } + ] +} diff --git a/packages/peer-collections/CHANGELOG.md b/packages/peer-collections/CHANGELOG.md new file mode 100644 index 0000000000..2c070f5975 --- /dev/null +++ b/packages/peer-collections/CHANGELOG.md @@ -0,0 +1,114 @@ +## [3.0.2](https://github.com/libp2p/js-libp2p-peer-collections/compare/v3.0.1...v3.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([ceeba09](https://github.com/libp2p/js-libp2p-peer-collections/commit/ceeba0909f8f5d2d9b239761530a45dcbc119f85)) +* Update .github/workflows/stale.yml [skip ci] ([9190f64](https://github.com/libp2p/js-libp2p-peer-collections/commit/9190f64e66f11090b63b2a7c6ee5fe1c5ed5327c)) + + +### Dependencies + +* **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)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-peer-collections/compare/v3.0.0...v3.0.1) (2023-03-24) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([aece586](https://github.com/libp2p/js-libp2p-peer-collections/commit/aece5866e1acd0e2ec189ecb189b04a864b2627b)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7cea5fc](https://github.com/libp2p/js-libp2p-peer-collections/commit/7cea5fc5b11f7f2326d5268119e7f02b18745352)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([67007a2](https://github.com/libp2p/js-libp2p-peer-collections/commit/67007a29585ce2d7a6396edbed85d8428cd10413)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.8 ([#28](https://github.com/libp2p/js-libp2p-peer-collections/issues/28)) ([17b6e75](https://github.com/libp2p/js-libp2p-peer-collections/commit/17b6e75131932f47d592d61dc517bc0439bfa318)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.2.2...v3.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update to multiformats 11 (#15) + +### Bug Fixes + +* update to multiformats 11 ([#15](https://github.com/libp2p/js-libp2p-peer-collections/issues/15)) ([a503803](https://github.com/libp2p/js-libp2p-peer-collections/commit/a5038039b48f165b3c7c4f5e114038cd71583fe3)) + +## [2.2.2](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.2.1...v2.2.2) (2022-12-16) + + +### Documentation + +* document mapIterable method ([5f2bc2c](https://github.com/libp2p/js-libp2p-peer-collections/commit/5f2bc2c22fbd1b53a715d28733dce6eb2daa4dab)) + +## [2.2.1](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.2.0...v2.2.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#14](https://github.com/libp2p/js-libp2p-peer-collections/issues/14)) ([f11b89a](https://github.com/libp2p/js-libp2p-peer-collections/commit/f11b89a3dec6431a34cfbbbde7c5e0e2878d440f)) + +## [2.2.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.1.0...v2.2.0) (2022-09-29) + + +### Features + +* add set operations - intersection, difference, union ([#9](https://github.com/libp2p/js-libp2p-peer-collections/issues/9)) ([896ee1f](https://github.com/libp2p/js-libp2p-peer-collections/commit/896ee1f537bfbdf7104b188edd7e858d02293a44)) + +## [2.1.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.0.0...v2.1.0) (2022-09-29) + + +### Features + +* accept Iterable in PeerList constructor ([#8](https://github.com/libp2p/js-libp2p-peer-collections/issues/8)) ([5596ede](https://github.com/libp2p/js-libp2p-peer-collections/commit/5596ede224a387181610353167d0dcfe27e2949f)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([615eacf](https://github.com/libp2p/js-libp2p-peer-collections/commit/615eacf6be0f3381756203521b28e1e44de58001)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v1.0.3...v2.0.0) (2022-06-28) + + +### ⚠ BREAKING CHANGES + +* uses new peer-id interface + +### Bug Fixes + +* update deps ([#5](https://github.com/libp2p/js-libp2p-peer-collections/issues/5)) ([89e975a](https://github.com/libp2p/js-libp2p-peer-collections/commit/89e975aece4bbe6f9783349b223d6dac3e1d3c78)) + +## [@libp2p/peer-collections-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-collections-v1.0.2...@libp2p/peer-collections-v1.0.3) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-collections-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-collections-v1.0.1...@libp2p/peer-collections-v1.0.2) (2022-04-19) + + +### Bug Fixes + +* move dev deps to prod ([#195](https://github.com/libp2p/js-libp2p-interfaces/issues/195)) ([3e1ffc7](https://github.com/libp2p/js-libp2p-interfaces/commit/3e1ffc7b174e74be483943ad4e5fcab823ae3f6d)) + +## [@libp2p/peer-collections-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-collections-v1.0.0...@libp2p/peer-collections-v1.0.1) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## @libp2p/peer-collections-v1.0.0 (2022-03-15) + + +### 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)) diff --git a/packages/peer-collections/LICENSE b/packages/peer-collections/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-collections/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-collections/LICENSE-APACHE b/packages/peer-collections/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-collections/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-collections/LICENSE-MIT b/packages/peer-collections/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-collections/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-collections/README.md b/packages/peer-collections/README.md new file mode 100644 index 0000000000..9292ed7402 --- /dev/null +++ b/packages/peer-collections/README.md @@ -0,0 +1,52 @@ +# @libp2p/peer-collections + +[![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) + +> Stores values against a peer id + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +We can't use PeerIds as collection keys because collection keys are compared using same-value-zero equality, so this is just a group of collections that stringifies PeerIds before storing them. + +PeerIds cache stringified versions of themselves so this should be a cheap operation. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-collections/package.json b/packages/peer-collections/package.json new file mode 100644 index 0000000000..b2b2990ab3 --- /dev/null +++ b/packages/peer-collections/package.json @@ -0,0 +1,61 @@ +{ + "name": "@libp2p/peer-collections", + "version": "3.0.2", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/peer-id": "^2.0.0" + }, + "devDependencies": { + "@libp2p/peer-id-factory": "^2.0.0", + "aegir": "^39.0.10" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-collections/src/index.ts b/packages/peer-collections/src/index.ts new file mode 100644 index 0000000000..db2767e619 --- /dev/null +++ b/packages/peer-collections/src/index.ts @@ -0,0 +1,3 @@ +export { PeerMap } from './map.js' +export { PeerSet } from './set.js' +export { PeerList } from './list.js' diff --git a/packages/peer-collections/src/list.ts b/packages/peer-collections/src/list.ts new file mode 100644 index 0000000000..562ef50a5f --- /dev/null +++ b/packages/peer-collections/src/list.ts @@ -0,0 +1,154 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { mapIterable } from './util.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * We can't use PeerIds as list entries because list entries are + * compared using same-value-zero equality, so this is just + * a map that stringifies the PeerIds before storing them. + * + * PeerIds cache stringified versions of themselves so this + * should be a cheap operation. + * + * @example + * + * ```JavaScript + * import { peerList } from '@libp2p/peer-collections' + * + * const list = peerList() + * list.push(peerId) + * ``` + */ +export class PeerList { + private readonly list: string[] + + constructor (list?: PeerList | Iterable) { + this.list = [] + + if (list != null) { + for (const value of list) { + this.list.push(value.toString()) + } + } + } + + [Symbol.iterator] (): IterableIterator { + return mapIterable<[number, string], PeerId>( + this.list.entries(), + (val) => { + return peerIdFromString(val[1]) + } + ) + } + + concat (list: PeerList): PeerList { + const output = new PeerList(this) + + for (const value of list) { + output.push(value) + } + + return output + } + + entries (): IterableIterator<[number, PeerId]> { + return mapIterable<[number, string], [number, PeerId]>( + this.list.entries(), + (val) => { + return [val[0], peerIdFromString(val[1])] + } + ) + } + + every (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): boolean { + return this.list.every((str, index) => { + return predicate(peerIdFromString(str), index, this) + }) + } + + filter (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): PeerList { + const output = new PeerList() + + this.list.forEach((str, index) => { + const peerId = peerIdFromString(str) + + if (predicate(peerId, index, this)) { + output.push(peerId) + } + }) + + return output + } + + find (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): PeerId | undefined { + const str = this.list.find((str, index) => { + return predicate(peerIdFromString(str), index, this) + }) + + if (str == null) { + return undefined + } + + return peerIdFromString(str) + } + + findIndex (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): number { + return this.list.findIndex((str, index) => { + return predicate(peerIdFromString(str), index, this) + }) + } + + forEach (predicate: (peerId: PeerId, index: number, arr: PeerList) => void): void { + this.list.forEach((str, index) => { + predicate(peerIdFromString(str), index, this) + }) + } + + includes (peerId: PeerId): boolean { + return this.list.includes(peerId.toString()) + } + + indexOf (peerId: PeerId): number { + return this.list.indexOf(peerId.toString()) + } + + pop (): PeerId | undefined { + const str = this.list.pop() + + if (str == null) { + return undefined + } + + return peerIdFromString(str) + } + + push (...peerIds: PeerId[]): void { + for (const peerId of peerIds) { + this.list.push(peerId.toString()) + } + } + + shift (): PeerId | undefined { + const str = this.list.shift() + + if (str == null) { + return undefined + } + + return peerIdFromString(str) + } + + unshift (...peerIds: PeerId[]): number { + let len = this.list.length + + for (let i = peerIds.length - 1; i > -1; i--) { + len = this.list.unshift(peerIds[i].toString()) + } + + return len + } + + get length (): number { + return this.list.length + } +} diff --git a/packages/peer-collections/src/map.ts b/packages/peer-collections/src/map.ts new file mode 100644 index 0000000000..aa54fe8465 --- /dev/null +++ b/packages/peer-collections/src/map.ts @@ -0,0 +1,90 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { mapIterable } from './util.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * We can't use PeerIds as map keys because map keys are + * compared using same-value-zero equality, so this is just + * a map that stringifies the PeerIds before storing them. + * + * PeerIds cache stringified versions of themselves so this + * should be a cheap operation. + * + * @example + * + * ```JavaScript + * import { peerMap } from '@libp2p/peer-collections' + * + * const map = peerMap() + * map.set(peerId, 'value') + * ``` + */ +export class PeerMap { + private readonly map: Map + + constructor (map?: PeerMap) { + this.map = new Map() + + if (map != null) { + for (const [key, value] of map.entries()) { + this.map.set(key.toString(), value) + } + } + } + + [Symbol.iterator] (): IterableIterator<[PeerId, T]> { + return this.entries() + } + + clear (): void { + this.map.clear() + } + + delete (peer: PeerId): void { + this.map.delete(peer.toString()) + } + + entries (): IterableIterator<[PeerId, T]> { + return mapIterable<[string, T], [PeerId, T]>( + this.map.entries(), + (val) => { + return [peerIdFromString(val[0]), val[1]] + } + ) + } + + forEach (fn: (value: T, key: PeerId, map: PeerMap) => void): void { + this.map.forEach((value, key) => { + fn(value, peerIdFromString(key), this) + }) + } + + get (peer: PeerId): T | undefined { + return this.map.get(peer.toString()) + } + + has (peer: PeerId): boolean { + return this.map.has(peer.toString()) + } + + set (peer: PeerId, value: T): void { + this.map.set(peer.toString(), value) + } + + keys (): IterableIterator { + return mapIterable( + this.map.keys(), + (val) => { + return peerIdFromString(val) + } + ) + } + + values (): IterableIterator { + return this.map.values() + } + + get size (): number { + return this.map.size + } +} diff --git a/packages/peer-collections/src/set.ts b/packages/peer-collections/src/set.ts new file mode 100644 index 0000000000..b238c3db65 --- /dev/null +++ b/packages/peer-collections/src/set.ts @@ -0,0 +1,124 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { mapIterable } from './util.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * We can't use PeerIds as set entries because set entries are + * compared using same-value-zero equality, so this is just + * a map that stringifies the PeerIds before storing them. + * + * PeerIds cache stringified versions of themselves so this + * should be a cheap operation. + * + * @example + * + * ```JavaScript + * import { peerSet } from '@libp2p/peer-collections' + * + * const set = peerSet() + * set.add(peerId) + * ``` + */ +export class PeerSet { + private readonly set: Set + + constructor (set?: PeerSet | Iterable) { + this.set = new Set() + + if (set != null) { + for (const key of set) { + this.set.add(key.toString()) + } + } + } + + get size (): number { + return this.set.size + } + + [Symbol.iterator] (): IterableIterator { + return this.values() + } + + add (peer: PeerId): void { + this.set.add(peer.toString()) + } + + clear (): void { + this.set.clear() + } + + delete (peer: PeerId): void { + this.set.delete(peer.toString()) + } + + entries (): IterableIterator<[PeerId, PeerId]> { + return mapIterable<[string, string], [PeerId, PeerId]>( + this.set.entries(), + (val) => { + const peerId = peerIdFromString(val[0]) + + return [peerId, peerId] + } + ) + } + + forEach (predicate: (peerId: PeerId, index: PeerId, set: PeerSet) => void): void { + this.set.forEach((str) => { + const id = peerIdFromString(str) + + predicate(id, id, this) + }) + } + + has (peer: PeerId): boolean { + return this.set.has(peer.toString()) + } + + values (): IterableIterator { + return mapIterable( + this.set.values(), + (val) => { + return peerIdFromString(val) + } + ) + } + + intersection (other: PeerSet): PeerSet { + const output = new PeerSet() + + for (const peerId of other) { + if (this.has(peerId)) { + output.add(peerId) + } + } + + return output + } + + difference (other: PeerSet): PeerSet { + const output = new PeerSet() + + for (const peerId of this) { + if (!other.has(peerId)) { + output.add(peerId) + } + } + + return output + } + + union (other: PeerSet): PeerSet { + const output = new PeerSet() + + for (const peerId of other) { + output.add(peerId) + } + + for (const peerId of this) { + output.add(peerId) + } + + return output + } +} diff --git a/packages/peer-collections/src/util.ts b/packages/peer-collections/src/util.ts new file mode 100644 index 0000000000..8a79a32c0f --- /dev/null +++ b/packages/peer-collections/src/util.ts @@ -0,0 +1,31 @@ + +/** + * Calls the passed map function on every entry of the passed iterable iterator + */ +export function mapIterable (iter: IterableIterator, map: (val: T) => R): IterableIterator { + const iterator: IterableIterator = { + [Symbol.iterator]: () => { + return iterator + }, + next: () => { + const next = iter.next() + const val = next.value + + if (next.done === true || val == null) { + const result: IteratorReturnResult = { + done: true, + value: undefined + } + + return result + } + + return { + done: false, + value: map(val) + } + } + } + + return iterator +} diff --git a/packages/peer-collections/test/list.spec.ts b/packages/peer-collections/test/list.spec.ts new file mode 100644 index 0000000000..665a5dcfb4 --- /dev/null +++ b/packages/peer-collections/test/list.spec.ts @@ -0,0 +1,35 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { PeerList } from '../src/index.js' + +describe('peer-list', () => { + it('should return a list', async () => { + const list = new PeerList() + const peer = await createEd25519PeerId() + + list.push(peer) + + const peer2 = peerIdFromBytes(peer.toBytes()) + + expect(list.indexOf(peer2)).to.equal(0) + }) + + it('should create a list with PeerList contents', async () => { + const list1 = new PeerList() + const peer = await createEd25519PeerId() + + list1.push(peer) + + const list2 = new PeerList(list1) + + expect(list2.indexOf(peer)).to.equal(0) + }) + + it('should create a list with Array contents', async () => { + const peer = await createEd25519PeerId() + const list = new PeerList([peer]) + + expect(list.indexOf(peer)).to.equal(0) + }) +}) diff --git a/packages/peer-collections/test/map.spec.ts b/packages/peer-collections/test/map.spec.ts new file mode 100644 index 0000000000..b501b73254 --- /dev/null +++ b/packages/peer-collections/test/map.spec.ts @@ -0,0 +1,30 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { PeerMap } from '../src/index.js' + +describe('peer-map', () => { + it('should return a map', async () => { + const map = new PeerMap() + const value = 5 + const peer = await createEd25519PeerId() + + map.set(peer, value) + + const peer2 = peerIdFromBytes(peer.toBytes()) + + expect(map.get(peer2)).to.equal(value) + }) + + it('should create a map with contents', async () => { + const map1 = new PeerMap() + const value = 5 + const peer = await createEd25519PeerId() + + map1.set(peer, value) + + const map2 = new PeerMap(map1) + + expect(map2.get(peer)).to.equal(value) + }) +}) diff --git a/packages/peer-collections/test/set.spec.ts b/packages/peer-collections/test/set.spec.ts new file mode 100644 index 0000000000..27e12fab6b --- /dev/null +++ b/packages/peer-collections/test/set.spec.ts @@ -0,0 +1,110 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { PeerSet } from '../src/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('peer-set', () => { + it('should return a set', async () => { + const set = new PeerSet() + const peer = await createEd25519PeerId() + + set.add(peer) + + const peer2 = peerIdFromBytes(peer.toBytes()) + + expect(set.has(peer2)).to.be.true() + }) + + it('should create a set with PeerSet contents', async () => { + const set1 = new PeerSet() + const peer = await createEd25519PeerId() + + set1.add(peer) + + const set2 = new PeerSet(set1) + + expect(set2.has(peer)).to.be.true() + }) + + it('should create a set with Array contents', async () => { + const peer = await createEd25519PeerId() + const set = new PeerSet([peer]) + + expect(set.has(peer)).to.be.true() + }) + + it('should create a set with Set contents', async () => { + const peer = await createEd25519PeerId() + const s = new Set() + s.add(peer) + const set = new PeerSet(s) + + expect(set.has(peer)).to.be.true() + }) + + it('should return intersection', async () => { + const peer1 = await createEd25519PeerId() + const peer2 = await createEd25519PeerId() + + const s1 = new PeerSet([peer1]) + const s2 = new PeerSet([peer1, peer2]) + + expect(s1.intersection(s2)).to.have.property('size', 1) + expect(s1.intersection(s2).has(peer1)).to.be.true() + expect(s1.intersection(s2).has(peer2)).to.be.false() + + expect(s2.intersection(s1)).to.have.property('size', 1) + expect(s2.intersection(s1).has(peer1)).to.be.true() + expect(s2.intersection(s1).has(peer2)).to.be.false() + + expect(s1.intersection(s1)).to.have.property('size', 1) + expect(s1.intersection(s1).has(peer1)).to.be.true() + expect(s1.intersection(s1).has(peer2)).to.be.false() + + expect(s2.intersection(s2)).to.have.property('size', 2) + expect(s2.intersection(s2).has(peer1)).to.be.true() + expect(s2.intersection(s2).has(peer2)).to.be.true() + }) + + it('should return difference', async () => { + const peer1 = await createEd25519PeerId() + const peer2 = await createEd25519PeerId() + + const s1 = new PeerSet([peer1]) + const s2 = new PeerSet([peer1, peer2]) + + expect(s1.difference(s2)).to.have.property('size', 0) + + expect(s2.difference(s1)).to.have.property('size', 1) + expect(s2.difference(s1).has(peer1)).to.be.false() + expect(s2.difference(s1).has(peer2)).to.be.true() + + expect(s1.difference(s1)).to.have.property('size', 0) + expect(s2.difference(s2)).to.have.property('size', 0) + }) + + it('should return union', async () => { + const peer1 = await createEd25519PeerId() + const peer2 = await createEd25519PeerId() + + const s1 = new PeerSet([peer1]) + const s2 = new PeerSet([peer1, peer2]) + + expect(s1.union(s2)).to.have.property('size', 2) + expect(s1.union(s2).has(peer1)).to.be.true() + expect(s1.union(s2).has(peer2)).to.be.true() + + expect(s2.union(s1)).to.have.property('size', 2) + expect(s2.union(s1).has(peer1)).to.be.true() + expect(s2.union(s1).has(peer2)).to.be.true() + + expect(s1.union(s1)).to.have.property('size', 1) + expect(s1.union(s1).has(peer1)).to.be.true() + expect(s1.union(s1).has(peer2)).to.be.false() + + expect(s2.union(s2)).to.have.property('size', 2) + expect(s2.union(s2).has(peer1)).to.be.true() + expect(s2.union(s2).has(peer2)).to.be.true() + }) +}) diff --git a/packages/peer-collections/tsconfig.json b/packages/peer-collections/tsconfig.json new file mode 100644 index 0000000000..6a53317368 --- /dev/null +++ b/packages/peer-collections/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/peer-discovery-bootstrap/CHANGELOG.md b/packages/peer-discovery-bootstrap/CHANGELOG.md new file mode 100644 index 0000000000..a74abf2fa2 --- /dev/null +++ b/packages/peer-discovery-bootstrap/CHANGELOG.md @@ -0,0 +1,477 @@ +## [8.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v7.0.1...v8.0.0) (2023-05-04) + + +### ⚠ BREAKING CHANGES + +* update @libp2p/interface-peer-discovery to 2.0.0 (#176) + +### Dependencies + +* 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)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v7.0.0...v7.0.1) (2023-05-04) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#175](https://github.com/libp2p/js-libp2p-bootstrap/issues/175)) ([e8d7ed7](https://github.com/libp2p/js-libp2p-bootstrap/commit/e8d7ed76e60b64c2e5a554cd10173a1176e5e2b1)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.3...v7.0.0) (2023-04-24) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 (#171) + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#171](https://github.com/libp2p/js-libp2p-bootstrap/issues/171)) ([9f36bb2](https://github.com/libp2p/js-libp2p-bootstrap/commit/9f36bb2d1f966b46d73b50a590f0141894ef511f)) + +## [6.0.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.2...v6.0.3) (2023-03-20) + + +### Dependencies + +* bump @multiformats/mafmt from 11.1.2 to 12.0.0 ([#170](https://github.com/libp2p/js-libp2p-bootstrap/issues/170)) ([5c15878](https://github.com/libp2p/js-libp2p-bootstrap/commit/5c158783314b9fdc9f69608cb8ebe180732c4242)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.1...v6.0.2) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#169](https://github.com/libp2p/js-libp2p-bootstrap/issues/169)) ([3532982](https://github.com/libp2p/js-libp2p-bootstrap/commit/35329826d9e90ec6c323d883e5838bd347dea71c)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.0...v6.0.1) (2023-03-17) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([2771c45](https://github.com/libp2p/js-libp2p-bootstrap/commit/2771c45b036be498416cebb4ecd017a5f57ed102)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([bdc51df](https://github.com/libp2p/js-libp2p-bootstrap/commit/bdc51df6aefc35fd8a55c1d134a4b5f3b6b2c7c2)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([93061bb](https://github.com/libp2p/js-libp2p-bootstrap/commit/93061bbf78a2033b2e67b4fd2fb53bf827ed409e)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#163](https://github.com/libp2p/js-libp2p-bootstrap/issues/163)) ([81ae1f2](https://github.com/libp2p/js-libp2p-bootstrap/commit/81ae1f246ae21b842ab002ad4dfb3107950fbaa6)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v5.0.2...v6.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to v11 (#153) + +### Bug Fixes + +* update multiformats to v11 ([#153](https://github.com/libp2p/js-libp2p-bootstrap/issues/153)) ([dca6305](https://github.com/libp2p/js-libp2p-bootstrap/commit/dca63051ef102e79ae712c8a786cb26fb42870d3)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v5.0.1...v5.0.2) (2022-12-16) + + +### Documentation + +* publish typedoc api docs ([#152](https://github.com/libp2p/js-libp2p-bootstrap/issues/152)) ([b0ff483](https://github.com/libp2p/js-libp2p-bootstrap/commit/b0ff4832751ef74eb012d5058d04e8b087894fe8)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v5.0.0...v5.0.1) (2022-12-01) + + +### Documentation + +* update readme ([#149](https://github.com/libp2p/js-libp2p-bootstrap/issues/149)) ([aba592f](https://github.com/libp2p/js-libp2p-bootstrap/commit/aba592f1c3e9e8687357aa9513de56ef6b22f812)), closes [#147](https://github.com/libp2p/js-libp2p-bootstrap/issues/147) + +## [5.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v4.0.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#144](https://github.com/libp2p/js-libp2p-bootstrap/issues/144)) ([772acc1](https://github.com/libp2p/js-libp2p-bootstrap/commit/772acc14a7eaab369bf7d885a96188853d353ed0)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v3.0.0...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#143) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#143](https://github.com/libp2p/js-libp2p-bootstrap/issues/143)) ([96f2a1e](https://github.com/libp2p/js-libp2p-bootstrap/commit/96f2a1ee7b93c7b74c7070db680e2738f9c2e516)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v2.0.1...v3.0.0) (2022-09-23) + + +### ⚠ BREAKING CHANGES + +* the `interval` option has been renamed `timeout` and +peers are now only discovered once + +### Bug Fixes + +* only discover bootstrap peers once and tag them on discovery ([#142](https://github.com/libp2p/js-libp2p-bootstrap/issues/142)) ([cd41d94](https://github.com/libp2p/js-libp2p-bootstrap/commit/cd41d94fa0fc1d84592448c6e9eccd65ad5e80b1)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v2.0.0...v2.0.1) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([fed165c](https://github.com/libp2p/js-libp2p-bootstrap/commit/fed165ca3396bb7a9b330b4dbe3a90d4af9cc703)) + + +### Dependencies + +* update @multiformats/multaddr to 11.0.0 ([#141](https://github.com/libp2p/js-libp2p-bootstrap/issues/141)) ([1c9079c](https://github.com/libp2p/js-libp2p-bootstrap/commit/1c9079c57ff8b12b97b1f6f3b3596c16f145846b)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.6...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#137](https://github.com/libp2p/js-libp2p-bootstrap/issues/137)) ([aabed1d](https://github.com/libp2p/js-libp2p-bootstrap/commit/aabed1db852ed804bb8d0de94b783bf451cdea85)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.5...v1.0.6) (2022-05-23) + + +### Bug Fixes + +* update interfaces ([#132](https://github.com/libp2p/js-libp2p-bootstrap/issues/132)) ([7d2edb3](https://github.com/libp2p/js-libp2p-bootstrap/commit/7d2edb3b7dc185076dbbbd7cec6edb5ee7b288ec)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.4...v1.0.5) (2022-05-06) + + +### Bug Fixes + +* update interfaces ([#129](https://github.com/libp2p/js-libp2p-bootstrap/issues/129)) ([31de4e1](https://github.com/libp2p/js-libp2p-bootstrap/commit/31de4e18fcaf93f987b1e9e1fcf834680c06c216)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.3...v1.0.4) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#128](https://github.com/libp2p/js-libp2p-bootstrap/issues/128)) ([a07f0e8](https://github.com/libp2p/js-libp2p-bootstrap/commit/a07f0e8a10d9fea533e299c0fb7284355989b043)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.2...v1.0.3) (2022-04-08) + + +### Trivial Changes + +* update aegir ([#127](https://github.com/libp2p/js-libp2p-bootstrap/issues/127)) ([c8e4f01](https://github.com/libp2p/js-libp2p-bootstrap/commit/c8e4f01da4fb6c2a19c1ad6bab40e316d23a9258)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.1...v1.0.2) (2022-03-24) + + +### Bug Fixes + +* update interfaces ([#123](https://github.com/libp2p/js-libp2p-bootstrap/issues/123)) ([4b7a52a](https://github.com/libp2p/js-libp2p-bootstrap/commit/4b7a52a374086c2ee874b1eb5d83d7a391885671)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.0...v1.0.1) (2022-03-17) + + +### Bug Fixes + +* update interfaces ([#121](https://github.com/libp2p/js-libp2p-bootstrap/issues/121)) ([4b5e757](https://github.com/libp2p/js-libp2p-bootstrap/commit/4b5e7576e67f6550d5e18f16afc121553936230b)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.14.0...v1.0.0) (2022-02-06) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +Co-authored-by: Alan Shaw + +### Features + +* convert to typescript ([#119](https://github.com/libp2p/js-libp2p-bootstrap/issues/119)) ([e4a0326](https://github.com/libp2p/js-libp2p-bootstrap/commit/e4a0326b6de88d36b752fa7143814da464252ce0)) + +# [0.14.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.13.0...v0.14.0) (2021-12-02) + + +### chore + +* update peer id, interfaces, etc ([#115](https://github.com/libp2p/js-libp2p-bootstrap/issues/115)) ([f7b8ce0](https://github.com/libp2p/js-libp2p-bootstrap/commit/f7b8ce0cea136f7258bc57ccdb294d9cf40ba811)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.12.3...v0.13.0) (2021-07-08) + + +### chore + +* update deps ([#114](https://github.com/libp2p/js-libp2p-bootstrap/issues/114)) ([597144f](https://github.com/libp2p/js-libp2p-bootstrap/commit/597144f9c0e0a9674c5e90595d516d191b83a11f)) + + +### BREAKING CHANGES + +* uses new peer-id, multiaddr and friends + + + +## [0.12.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.12.2...v0.12.3) (2021-04-13) + + +### Bug Fixes + +* build ([#113](https://github.com/libp2p/js-libp2p-bootstrap/issues/113)) ([aeab2bf](https://github.com/libp2p/js-libp2p-bootstrap/commit/aeab2bf46dfd5d7026e9e2b06be9c0b88bd75de1)) + + + +## [0.12.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.12.1...v0.12.2) (2021-02-08) + + +### Features + +* add types and update deps ([#111](https://github.com/libp2p/js-libp2p-bootstrap/issues/111)) ([269b807](https://github.com/libp2p/js-libp2p-bootstrap/commit/269b80782c4640dbbb7d66de0345703086c03f24)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.11.0...v0.12.1) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#106](https://github.com/libp2p/js-libp2p-bootstrap/issues/106)) ([b59b7ad](https://github.com/libp2p/js-libp2p-bootstrap/commit/b59b7ad)) + + +### BREAKING CHANGES + +* - The deps of this module have Uint8Array properties + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.11.0...v0.12.0) (2020-08-10) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#106](https://github.com/libp2p/js-libp2p-bootstrap/issues/106)) ([b59b7ad](https://github.com/libp2p/js-libp2p-bootstrap/commit/b59b7ad)) + + +### BREAKING CHANGES + +* - The deps of this module have Uint8Array properties + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.4...v0.11.0) (2020-04-21) + + +### Chores + +* peer-discovery not using peer-info ([8a99f1b](https://github.com/libp2p/js-libp2p-bootstrap/commit/8a99f1b)) + + +### BREAKING CHANGES + +* peer event emits an object with id and multiaddr instead of a peer-info + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.3...v0.10.4) (2020-02-14) + + +### Bug Fixes + +* remove use of assert module ([#99](https://github.com/libp2p/js-libp2p-bootstrap/issues/99)) ([29b8aa6](https://github.com/libp2p/js-libp2p-bootstrap/commit/29b8aa6)) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.2...v0.10.3) (2019-11-28) + + +### Bug Fixes + +* validate list ([#97](https://github.com/libp2p/js-libp2p-bootstrap/issues/97)) ([5041f28](https://github.com/libp2p/js-libp2p-bootstrap/commit/5041f28)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.1...v0.10.2) (2019-08-01) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.0...v0.10.1) (2019-07-31) + + +### Bug Fixes + +* use callback in start from js-libp2p ([#93](https://github.com/libp2p/js-libp2p-bootstrap/issues/93)) ([74c305d](https://github.com/libp2p/js-libp2p-bootstrap/commit/74c305d)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.7...v0.10.0) (2019-07-15) + + +### Code Refactoring + +* callbacks -> async/await ([#89](https://github.com/libp2p/js-libp2p-bootstrap/issues/89)) ([77cfc28](https://github.com/libp2p/js-libp2p-bootstrap/commit/77cfc28)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await + + + + +## [0.9.7](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.6...v0.9.7) (2019-01-10) + + + + +## [0.9.6](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.5...v0.9.6) (2019-01-04) + + + + +## [0.9.5](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.4...v0.9.5) (2019-01-03) + + +### Bug Fixes + +* discover peers faster ([#86](https://github.com/libp2p/js-libp2p-bootstrap/issues/86)) ([63a6d10](https://github.com/libp2p/js-libp2p-bootstrap/commit/63a6d10)), closes [#85](https://github.com/libp2p/js-libp2p-bootstrap/issues/85) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.3...v0.9.4) (2018-11-26) + + +### Bug Fixes + +* rename railing -> bootstrap ([#81](https://github.com/libp2p/js-libp2p-bootstrap/issues/81)) ([bda0dc8](https://github.com/libp2p/js-libp2p-bootstrap/commit/bda0dc8)) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.2...v0.9.3) (2018-07-02) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-railing/compare/v0.9.1...v0.9.2) (2018-06-29) + + +### Bug Fixes + +* name of property and make it stop properly ([#77](https://github.com/libp2p/js-libp2p-railing/issues/77)) ([8f9bef6](https://github.com/libp2p/js-libp2p-railing/commit/8f9bef6)) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.9.0...v0.9.1) (2018-06-05) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.8.1...v0.9.0) (2018-06-05) + + +### Features + +* (BREAKING CHANGE) constructor takes options. + add tag, update deps and fix tests ([27f9aed](https://github.com/libp2p/js-libp2p-railing/commit/27f9aed)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.8.0...v0.8.1) (2018-04-12) + + +### Bug Fixes + +* add more error handling for malformed bootstrap multiaddr ([#74](https://github.com/libp2p/js-libp2p-railing/issues/74)) ([f65e1ba](https://github.com/libp2p/js-libp2p-railing/commit/f65e1ba)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.7.1...v0.8.0) (2018-04-05) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.7.0...v0.7.1) (2017-09-08) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.6.1...v0.7.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#70](https://github.com/libp2p/js-libp2p-railing/issues/70)) ([34064b2](https://github.com/libp2p/js-libp2p-railing/commit/34064b2)) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.6.0...v0.6.1) (2017-07-23) + + +### Features + +* emit peers every 10 secs ([598fd94](https://github.com/libp2p/js-libp2p-railing/commit/598fd94)) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.5.2...v0.6.0) (2017-07-22) + + + + +## [0.5.2](https://github.com/libp2p/js-libp2p-railing/compare/v0.5.1...v0.5.2) (2017-07-08) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.5.0...v0.5.1) (2017-05-19) + + +### Bug Fixes + +* use async/setImmediate ([0c6f754](https://github.com/libp2p/js-libp2p-railing/commit/0c6f754)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.4.3...v0.5.0) (2017-03-30) + + +### Features + +* update to new peer-info ([a6254d8](https://github.com/libp2p/js-libp2p-railing/commit/a6254d8)) + + + + +## [0.4.3](https://github.com/libp2p/js-libp2p-railing/compare/v0.4.2...v0.4.3) (2017-03-23) + + +### Bug Fixes + +* multiaddr parsing ([#53](https://github.com/libp2p/js-libp2p-railing/issues/53)) ([7d13ea6](https://github.com/libp2p/js-libp2p-railing/commit/7d13ea6)) + + + + +## [0.4.2](https://github.com/libp2p/js-ipfs-railing/compare/v0.4.1...v0.4.2) (2017-03-21) diff --git a/packages/peer-discovery-bootstrap/LICENSE b/packages/peer-discovery-bootstrap/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-discovery-bootstrap/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-discovery-bootstrap/LICENSE-APACHE b/packages/peer-discovery-bootstrap/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-discovery-bootstrap/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-discovery-bootstrap/LICENSE-MIT b/packages/peer-discovery-bootstrap/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-discovery-bootstrap/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-discovery-bootstrap/README.md b/packages/peer-discovery-bootstrap/README.md new file mode 100644 index 0000000000..c48965d55c --- /dev/null +++ b/packages/peer-discovery-bootstrap/README.md @@ -0,0 +1,103 @@ +# @libp2p/bootstrap + +[![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) + +> Peer discovery via a list of bootstrap peers + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Usage + +The configured bootstrap peers will be discovered after the configured timeout. This will ensure +there are some peers in the peer store for the node to use to discover other peers. + +They will be tagged with a tag with the name `'bootstrap'` tag, the value `50` and it will expire +after two minutes which means the nodes connections may be closed if the maximum number of +connections is reached. + +Clients that need constant connections to bootstrap nodes (e.g. browsers) can set the TTL to `Infinity`. + +```JavaScript +import { createLibp2p } from 'libp2p' +import { bootstrap } from '@libp2p/bootstrap' +import { tcp } from 'libp2p/tcp' +import { noise } from '@libp2p/noise' +import { mplex } from '@libp2p/mplex' + +let options = { + transports: [ + tcp() + ], + streamMuxers: [ + mplex() + ], + connectionEncryption: [ + noise() + ], + peerDiscovery: [ + bootstrap({ + list: [ // a list of bootstrap peer multiaddrs to connect to on node startup + "/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", + "/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa" + ], + timeout: 1000, // in ms, + tagName: 'bootstrap', + tagValue: 50, + tagTTL: 120000 // in ms + }) + ] +} + +async function start () { + let libp2p = await createLibp2p(options) + + libp2p.on('peer:discovery', function (peerId) { + console.log('found peer: ', peerId.toB58String()) + }) + + await libp2p.start() + +} + +start() +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-discovery-bootstrap/package.json b/packages/peer-discovery-bootstrap/package.json new file mode 100644 index 0000000000..792fcabd24 --- /dev/null +++ b/packages/peer-discovery-bootstrap/package.json @@ -0,0 +1,69 @@ +{ + "name": "@libp2p/bootstrap", + "version": "8.0.0", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-discovery": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3" + }, + "devDependencies": { + "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "aegir": "^39.0.10", + "sinon-ts": "^1.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-discovery-bootstrap/src/index.ts b/packages/peer-discovery-bootstrap/src/index.ts new file mode 100644 index 0000000000..e283223b21 --- /dev/null +++ b/packages/peer-discovery-bootstrap/src/index.ts @@ -0,0 +1,164 @@ +import { peerDiscovery } from '@libp2p/interface-peer-discovery' +import { EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { P2P } from '@multiformats/mafmt' +import { multiaddr } from '@multiformats/multiaddr' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Startable } from '@libp2p/interfaces/dist/src/startable' + +const log = logger('libp2p:bootstrap') + +const DEFAULT_BOOTSTRAP_TAG_NAME = 'bootstrap' +const DEFAULT_BOOTSTRAP_TAG_VALUE = 50 +const DEFAULT_BOOTSTRAP_TAG_TTL = 120000 +const DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT = 1000 + +export interface BootstrapInit { + /** + * The list of peer addresses in multi-address format + */ + list: string[] + + /** + * How long to wait before discovering bootstrap nodes + */ + timeout?: number + + /** + * Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap') + */ + tagName?: string + + /** + * The bootstrap peer tag will have this value (default: 50) + */ + tagValue?: number + + /** + * Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes) + */ + tagTTL?: number +} + +export interface BootstrapComponents { + peerStore: PeerStore +} + +/** + * Emits 'peer' events on a regular interval for each peer in the provided list. + */ +class Bootstrap extends EventEmitter implements PeerDiscovery, Startable { + static tag = 'bootstrap' + + private timer?: ReturnType + private readonly list: PeerInfo[] + private readonly timeout: number + private readonly components: BootstrapComponents + private readonly _init: BootstrapInit + + constructor (components: BootstrapComponents, options: BootstrapInit = { list: [] }) { + if (options.list == null || options.list.length === 0) { + throw new Error('Bootstrap requires a list of peer addresses') + } + super() + + this.components = components + this.timeout = options.timeout ?? DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT + this.list = [] + + for (const candidate of options.list) { + if (!P2P.matches(candidate)) { + log.error('Invalid multiaddr') + continue + } + + const ma = multiaddr(candidate) + const peerIdStr = ma.getPeerId() + + if (peerIdStr == null) { + log.error('Invalid bootstrap multiaddr without peer id') + continue + } + + const peerData: PeerInfo = { + id: peerIdFromString(peerIdStr), + multiaddrs: [ma], + protocols: [] + } + + this.list.push(peerData) + } + + this._init = options + } + + readonly [peerDiscovery] = this + + readonly [Symbol.toStringTag] = '@libp2p/bootstrap' + + isStarted (): boolean { + return Boolean(this.timer) + } + + /** + * Start emitting events + */ + start (): void { + if (this.isStarted()) { + return + } + + log('Starting bootstrap node discovery, discovering peers after %s ms', this.timeout) + this.timer = setTimeout(() => { + void this._discoverBootstrapPeers() + .catch(err => { + log.error(err) + }) + }, this.timeout) + } + + /** + * Emit each address in the list as a PeerInfo + */ + async _discoverBootstrapPeers (): Promise { + if (this.timer == null) { + return + } + + for (const peerData of this.list) { + await this.components.peerStore.merge(peerData.id, { + tags: { + [this._init.tagName ?? DEFAULT_BOOTSTRAP_TAG_NAME]: { + value: this._init.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE, + ttl: this._init.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL + } + } + }) + + // check we are still running + if (this.timer == null) { + return + } + + this.safeDispatchEvent('peer', { detail: peerData }) + } + } + + /** + * Stop emitting events + */ + stop (): void { + if (this.timer != null) { + clearTimeout(this.timer) + } + + this.timer = undefined + } +} + +export function bootstrap (init: BootstrapInit): (components: BootstrapComponents) => PeerDiscovery { + return (components: BootstrapComponents) => new Bootstrap(components, init) +} diff --git a/packages/peer-discovery-bootstrap/test/bootstrap.spec.ts b/packages/peer-discovery-bootstrap/test/bootstrap.spec.ts new file mode 100644 index 0000000000..4213385204 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/bootstrap.spec.ts @@ -0,0 +1,125 @@ +/* eslint-env mocha */ + +import { isPeerId } from '@libp2p/interface-peer-id' +import { start, stop } from '@libp2p/interfaces/startable' +import { peerIdFromString } from '@libp2p/peer-id' +import { IPFS } from '@multiformats/mafmt' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { type StubbedInstance, stubInterface } from 'sinon-ts' +import { bootstrap, type BootstrapComponents } from '../src/index.js' +import peerList from './fixtures/default-peers.js' +import partialValidPeerList from './fixtures/some-invalid-peers.js' +import type { PeerStore } from '@libp2p/interface-peer-store' + +describe('bootstrap', () => { + let components: BootstrapComponents + let peerStore: StubbedInstance + + beforeEach(async () => { + peerStore = stubInterface() + + components = { + peerStore + } + }) + + it('should throw if no peer list is provided', () => { + // @ts-expect-error missing args + expect(() => bootstrap()()) + .to.throw('Bootstrap requires a list of peer addresses') + }) + + it('should discover bootstrap peers', async function () { + this.timeout(5 * 1000) + const r = bootstrap({ + list: peerList, + timeout: 100 + })(components) + + const p = new Promise((resolve) => { + r.addEventListener('peer', resolve, { + once: true + }) + }) + await start(r) + + await p + await stop(r) + }) + + it('should tag bootstrap peers', async function () { + this.timeout(5 * 1000) + + const tagName = 'tag-tag' + const tagValue = 10 + const tagTTL = 50 + + const r = bootstrap({ + list: peerList, + timeout: 100, + tagName, + tagValue, + tagTTL + })(components) + + const p = new Promise((resolve) => { + r.addEventListener('peer', resolve, { + once: true + }) + }) + await start(r) + + await p + + const bootstrapper0ma = multiaddr(peerList[0]) + const bootstrapper0PeerIdStr = bootstrapper0ma.getPeerId() + + if (bootstrapper0PeerIdStr == null) { + throw new Error('bootstrapper had no PeerID') + } + + const bootstrapper0PeerId = peerIdFromString(bootstrapper0PeerIdStr) + + expect(peerStore.merge).to.have.property('called', true) + + const call = peerStore.merge.getCall(0) + expect(call).to.have.deep.nested.property('args[0]', bootstrapper0PeerId) + expect(call).to.have.deep.nested.property('args[1]', { + tags: { + [tagName]: { + value: tagValue, + ttl: tagTTL + } + } + }) + + await stop(r) + }) + + it('should not fail on malformed peers in peer list', async function () { + this.timeout(5 * 1000) + + const r = bootstrap({ + list: partialValidPeerList, + timeout: 100 + })(components) + + const p = new Promise((resolve) => { + r.addEventListener('peer', (evt) => { + const { id, multiaddrs } = evt.detail + + expect(id).to.exist() + expect(isPeerId(id)).to.be.true() + expect(multiaddrs.length).to.eq(1) + expect(IPFS.matches(multiaddrs[0].toString())).equals(true) + resolve() + }) + }) + + await start(r) + + await p + await stop(r) + }) +}) diff --git a/packages/peer-discovery-bootstrap/test/compliance.spec.ts b/packages/peer-discovery-bootstrap/test/compliance.spec.ts new file mode 100644 index 0000000000..7afc7dba12 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/compliance.spec.ts @@ -0,0 +1,23 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-peer-discovery-compliance-tests' +import { stubInterface } from 'sinon-ts' +import { bootstrap } from '../src/index.js' +import peerList from './fixtures/default-peers.js' +import type { PeerStore } from '@libp2p/interface-peer-store' + +describe('compliance tests', () => { + tests({ + async setup () { + const components = { + peerStore: stubInterface() + } + + return bootstrap({ + list: peerList, + timeout: 100 + })(components) + }, + async teardown () {} + }) +}) diff --git a/packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts b/packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts new file mode 100644 index 0000000000..3b611d6463 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts @@ -0,0 +1,10 @@ +const peers: string[] = [ + '/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp' +] + +export default peers diff --git a/packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts b/packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts new file mode 100644 index 0000000000..2449b3a737 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts @@ -0,0 +1,9 @@ +const peers: string[] = [ + // @ts-expect-error this is an invalid peer + null, + '/ip4/104.236.151.122/tcp/4001/ipfs/malformed-peer-id', + '/ip4/bad.ip.addr/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + '/ip4/104.236.151.122/tcp/4001/ipfs/QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx' +] + +export default peers diff --git a/packages/peer-discovery-bootstrap/tsconfig.json b/packages/peer-discovery-bootstrap/tsconfig.json new file mode 100644 index 0000000000..954f3f631c --- /dev/null +++ b/packages/peer-discovery-bootstrap/tsconfig.json @@ -0,0 +1,38 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-discovery" + }, + { + "path": "../interface-peer-discovery-compliance-tests" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-info" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/peer-discovery-mdns/.aegir.js b/packages/peer-discovery-mdns/.aegir.js new file mode 100644 index 0000000000..4c82ade3d7 --- /dev/null +++ b/packages/peer-discovery-mdns/.aegir.js @@ -0,0 +1,8 @@ +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + config: { + platform: 'node' + } + } +} diff --git a/packages/peer-discovery-mdns/CHANGELOG.md b/packages/peer-discovery-mdns/CHANGELOG.md new file mode 100644 index 0000000000..faba3af6eb --- /dev/null +++ b/packages/peer-discovery-mdns/CHANGELOG.md @@ -0,0 +1,500 @@ +## [8.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.5...v8.0.0) (2023-05-04) + + +### ⚠ BREAKING CHANGES + +* update @libp2p/interface-peer-discovery to 2.0.0 (#197) + +### Dependencies + +* 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)) + +## [7.0.5](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.4...v7.0.5) (2023-05-04) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#196](https://github.com/libp2p/js-libp2p-mdns/issues/196)) ([1db5304](https://github.com/libp2p/js-libp2p-mdns/commit/1db530413904d24cfb446513769f45454ad82428)) + +## [7.0.4](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.3...v7.0.4) (2023-04-24) + + +### Dependencies + +* **dev:** bump @libp2p/interface-address-manager from 2.0.5 to 3.0.0 ([#193](https://github.com/libp2p/js-libp2p-mdns/issues/193)) ([5044392](https://github.com/libp2p/js-libp2p-mdns/commit/5044392191b4ea3149c2e4c6c75120eaa1441fde)) + +## [7.0.3](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.2...v7.0.3) (2023-04-07) + + +### Bug Fixes + +* do not append peer id to advertised addresses ([#192](https://github.com/libp2p/js-libp2p-mdns/issues/192)) ([d1ee623](https://github.com/libp2p/js-libp2p-mdns/commit/d1ee6236f678166e87d0784f041ad5588801fdf2)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.1...v7.0.2) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#191](https://github.com/libp2p/js-libp2p-mdns/issues/191)) ([25e353b](https://github.com/libp2p/js-libp2p-mdns/commit/25e353b1b1a5261ceb484acde924d8f007238326)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.0...v7.0.1) (2023-03-17) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([5e82b6d](https://github.com/libp2p/js-libp2p-mdns/commit/5e82b6d1e81934588bf70bdf8663672cf36bbb18)) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#190](https://github.com/libp2p/js-libp2p-mdns/issues/190)) ([6b4882f](https://github.com/libp2p/js-libp2p-mdns/commit/6b4882f58f8e9528ca405534afb9792c8988b339)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v6.0.0...v7.0.0) (2023-03-07) + + +### ⚠ BREAKING CHANGES + +* service name now defaults to `_p2p._udp.local` and no +longer uses A and AAA records -> replaced by TXT records + +Added random peer name option + +### Features + +* update to latest spec, added peer name, announces all multiaddrs ([#157](https://github.com/libp2p/js-libp2p-mdns/issues/157)) ([5edcc16](https://github.com/libp2p/js-libp2p-mdns/commit/5edcc16d119ebd2b644f85a29596fdcd33617bd0)), closes [#101](https://github.com/libp2p/js-libp2p-mdns/issues/101) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([28c668e](https://github.com/libp2p/js-libp2p-mdns/commit/28c668e9eee0906d4a05a27d824f1c293e702940)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([9dccd84](https://github.com/libp2p/js-libp2p-mdns/commit/9dccd84725704e2b3b6b7b2aee16829ca416904f)) +* upgrade aegir to `38.1.2` ([#182](https://github.com/libp2p/js-libp2p-mdns/issues/182)) ([f86328c](https://github.com/libp2p/js-libp2p-mdns/commit/f86328c5cdb4c5a83ee0c941feba3b6ef8e5c016)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v5.1.1...v6.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to v11.x.x (#178) + +### Bug Fixes + +* update multiformats to v11.x.x ([#178](https://github.com/libp2p/js-libp2p-mdns/issues/178)) ([e48b9b1](https://github.com/libp2p/js-libp2p-mdns/commit/e48b9b199a1a76893f888368b4a027df9ac0c4cf)) + +## [5.1.1](https://github.com/libp2p/js-libp2p-mdns/compare/v5.1.0...v5.1.1) (2022-12-16) + + +### Documentation + +* publish typedocs ([#176](https://github.com/libp2p/js-libp2p-mdns/issues/176)) ([7f3a41b](https://github.com/libp2p/js-libp2p-mdns/commit/7f3a41b59a85f618e0af8af9cbda77961edab334)) + + +### Trivial Changes + +* remove lockfile ([3c1b399](https://github.com/libp2p/js-libp2p-mdns/commit/3c1b39962442869f6f00a748d86d60f3aa204684)) + +## [5.1.0](https://github.com/libp2p/js-libp2p-mdns/compare/v5.0.0...v5.1.0) (2022-10-27) + + +### Features + +* add support for custom DNS server IP ([#142](https://github.com/libp2p/js-libp2p-mdns/issues/142)) ([3b6c7db](https://github.com/libp2p/js-libp2p-mdns/commit/3b6c7dbb0e6cfd11d1394ac3e62509926346dbf2)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v4.0.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#146](https://github.com/libp2p/js-libp2p-mdns/issues/146)) ([36d68fc](https://github.com/libp2p/js-libp2p-mdns/commit/36d68fc819316ec7f7a215a38310d90130770e0f)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v3.0.1...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#143) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#143](https://github.com/libp2p/js-libp2p-mdns/issues/143)) ([a6c3f22](https://github.com/libp2p/js-libp2p-mdns/commit/a6c3f22a68c9ea6e5431d3a34e16f67e1e4b9cff)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v3.0.0...v3.0.1) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([cfb0b5c](https://github.com/libp2p/js-libp2p-mdns/commit/cfb0b5cb007b18e5a508d2b11856bbdf895c72d8)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#140](https://github.com/libp2p/js-libp2p-mdns/issues/140)) ([931be6b](https://github.com/libp2p/js-libp2p-mdns/commit/931be6b3fce395ba2e66e9b811b6fb85b7d40083)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v2.0.1...v3.0.0) (2022-07-01) + + +### ⚠ BREAKING CHANGES + +* **deps:** uses components with single-issue interfaces + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +### Trivial Changes + +* **deps:** bump @libp2p/components from 1.0.0 to 2.0.1 ([#136](https://github.com/libp2p/js-libp2p-mdns/issues/136)) ([5c6d17b](https://github.com/libp2p/js-libp2p-mdns/commit/5c6d17bce713d9f404d01c08d5732e871c2151b1)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v2.0.0...v2.0.1) (2022-06-27) + + +### Bug Fixes + +* add @types/multicast-dns as dependency ([#135](https://github.com/libp2p/js-libp2p-mdns/issues/135)) ([8c855a1](https://github.com/libp2p/js-libp2p-mdns/commit/8c855a12336a341d3d53d6c15823c0be4a11b75e)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.7...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#131](https://github.com/libp2p/js-libp2p-mdns/issues/131)) ([c9d1e62](https://github.com/libp2p/js-libp2p-mdns/commit/c9d1e62381712678e7b4869a058cf60db3b40af6)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.6...v1.0.7) (2022-05-23) + + +### Bug Fixes + +* update deps ([#127](https://github.com/libp2p/js-libp2p-mdns/issues/127)) ([1f8654e](https://github.com/libp2p/js-libp2p-mdns/commit/1f8654e9387e5987714142e79038a16d5a1f94ac)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.5...v1.0.6) (2022-05-06) + + +### Bug Fixes + +* update interfaces ([#124](https://github.com/libp2p/js-libp2p-mdns/issues/124)) ([bbb0c62](https://github.com/libp2p/js-libp2p-mdns/commit/bbb0c62e0456044383a684ac8a271136360ee565)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.4...v1.0.5) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#123](https://github.com/libp2p/js-libp2p-mdns/issues/123)) ([d2692a1](https://github.com/libp2p/js-libp2p-mdns/commit/d2692a101965d233922d6b66d640928b1bd9ab74)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.3...v1.0.4) (2022-04-09) + + +### Trivial Changes + +* update aegir ([#122](https://github.com/libp2p/js-libp2p-mdns/issues/122)) ([37b689f](https://github.com/libp2p/js-libp2p-mdns/commit/37b689fb7e22887b050a6177bf05f9fc304563f9)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.2...v1.0.3) (2022-03-24) + + +### Bug Fixes + +* update interfaces ([#117](https://github.com/libp2p/js-libp2p-mdns/issues/117)) ([d454b94](https://github.com/libp2p/js-libp2p-mdns/commit/d454b94738ba3caf1b2e3da7cd43dd8f1863ed6d)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.1...v1.0.2) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#116](https://github.com/libp2p/js-libp2p-mdns/issues/116)) ([30ec23a](https://github.com/libp2p/js-libp2p-mdns/commit/30ec23a5bc2e983fe01e0e47e46ecedf4c0eab5d)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.0...v1.0.1) (2022-02-12) + + +### Bug Fixes + +* update to latest interfaces ([#114](https://github.com/libp2p/js-libp2p-mdns/issues/114)) ([4322c1e](https://github.com/libp2p/js-libp2p-mdns/commit/4322c1e8462cb3dde16435efc23303923bbc7d86)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.18.0...v1.0.0) (2022-02-11) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#113](https://github.com/libp2p/js-libp2p-mdns/issues/113)) ([296791e](https://github.com/libp2p/js-libp2p-mdns/commit/296791ec3364199ea5a2de6ee6fec0aadf318392)) + +# [0.18.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.17.0...v0.18.0) (2021-12-02) + + +### chore + +* update to new peer-id ([#102](https://github.com/libp2p/js-libp2p-mdns/issues/102)) ([d88eda5](https://github.com/libp2p/js-libp2p-mdns/commit/d88eda5fca9a589ecba519be89150f25a36271e6)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.16.0...v0.17.0) (2021-07-08) + + +### chore + +* update deps ([#100](https://github.com/libp2p/js-libp2p-mdns/issues/100)) ([0b974bc](https://github.com/libp2p/js-libp2p-mdns/commit/0b974bc9e0d110303e2d15a173447ec5631d15f9)) + + +### BREAKING CHANGES + +* uses then new multiaddr and friends + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.3...v0.16.0) (2021-04-13) + + +### Bug Fixes + +* actually check tcp multiaddrs ([#94](https://github.com/libp2p/js-libp2p-mdns/issues/94)) ([9f45f73](https://github.com/libp2p/js-libp2p-mdns/commit/9f45f731e91f225016d11cc3471bd0874e4b5490)) +* ensure event handlers are removed on MulticastDNS.stop ([#96](https://github.com/libp2p/js-libp2p-mdns/issues/96)) ([9fea1f6](https://github.com/libp2p/js-libp2p-mdns/commit/9fea1f6eb9d68b8e7c145e8b615ac504e0031b0e)) + + +### chore + +* peer-discovery not using peer-info ([#90](https://github.com/libp2p/js-libp2p-mdns/issues/90)) ([fca175e](https://github.com/libp2p/js-libp2p-mdns/commit/fca175e6bc706be07a14b81ef3b3c8143ce97a0a)) + + +### BREAKING CHANGES + +* peer event emitted with id and multiaddrs properties instead of peer-info + +* chore: add tests for peer-discovery interface + +* chore: apply suggestions from code review + +Co-Authored-By: Jacob Heun + +* chore: update readme with peerData and peerId + +Co-authored-by: Jacob Heun + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.3...v0.15.0) (2020-08-11) + + +### Chores + +* upgrade deps ([#97](https://github.com/libp2p/js-libp2p-mdns/issues/97)) ([3cf0e75](https://github.com/libp2p/js-libp2p-mdns/commit/3cf0e75)) + + +### BREAKING CHANGES + +* - All deps of this module now use Uint8Arrays instead of node Buffers + +* chore: address pr comments + + + + +## [0.14.3](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.2...v0.14.3) (2020-08-07) + + +### Bug Fixes + +* ensure event handlers are removed on MulticastDNS.stop ([#96](https://github.com/libp2p/js-libp2p-mdns/issues/96)) ([9fea1f6](https://github.com/libp2p/js-libp2p-mdns/commit/9fea1f6)) + + + + +## [0.14.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.1...v0.14.2) (2020-07-02) + + +### Bug Fixes + +* actually check tcp multiaddrs ([#94](https://github.com/libp2p/js-libp2p-mdns/issues/94)) ([9f45f73](https://github.com/libp2p/js-libp2p-mdns/commit/9f45f73)) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.0...v0.14.1) (2020-04-29) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.3...v0.14.0) (2020-04-23) + + +### Chores + +* peer-discovery not using peer-info ([#90](https://github.com/libp2p/js-libp2p-mdns/issues/90)) ([fca175e](https://github.com/libp2p/js-libp2p-mdns/commit/fca175e)) + + +### BREAKING CHANGES + +* peer event emitted with id and multiaddrs properties instead of peer-info + +* chore: add tests for peer-discovery interface + +* chore: apply suggestions from code review + +Co-Authored-By: Jacob Heun + +* chore: update readme with peerData and peerId + +Co-authored-by: Jacob Heun + + + + +## [0.13.3](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.2...v0.13.3) (2020-02-17) + + +### Bug Fixes + +* remove use of assert module ([#87](https://github.com/libp2p/js-libp2p-mdns/issues/87)) ([e362b04](https://github.com/libp2p/js-libp2p-mdns/commit/e362b04)) + + + + +## [0.13.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.1...v0.13.2) (2020-02-02) + + + + +## [0.13.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.0...v0.13.1) (2020-01-17) + + +### Bug Fixes + +* do not emit empty peer info objects ([#85](https://github.com/libp2p/js-libp2p-mdns/issues/85)) ([a88483c](https://github.com/libp2p/js-libp2p-mdns/commit/a88483c)) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.3...v0.13.0) (2019-09-27) + + +### Code Refactoring + +* callbacks -> async / await ([#78](https://github.com/libp2p/js-libp2p-mdns/issues/78)) ([46d78eb](https://github.com/libp2p/js-libp2p-mdns/commit/46d78eb)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await + +* chore: update CI file +* test: add compliance tests + + + + +## [0.12.3](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.2...v0.12.3) (2019-05-09) + + +### Features + +* compatibility with go-libp2p-mdns ([#80](https://github.com/libp2p/js-libp2p-mdns/issues/80)) ([c6d1d49](https://github.com/libp2p/js-libp2p-mdns/commit/c6d1d49)) + + + + +## [0.12.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.1...v0.12.2) (2019-01-04) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.0...v0.12.1) (2018-11-26) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.11.0...v0.12.0) (2018-06-05) + + +### Features + +* (BREAKING CHANGE) update constructor. add tag ([d3eeb6e](https://github.com/libp2p/js-libp2p-mdns/commit/d3eeb6e)) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.2...v0.11.0) (2018-04-05) + + +### Features + +* service names ([#68](https://github.com/libp2p/js-libp2p-mdns/issues/68)) ([fa8fe22](https://github.com/libp2p/js-libp2p-mdns/commit/fa8fe22)) +* Use latest multicast-dns and dns-packet ([#69](https://github.com/libp2p/js-libp2p-mdns/issues/69)) ([cb69f2f](https://github.com/libp2p/js-libp2p-mdns/commit/cb69f2f)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.2...v0.10.0) (2018-04-05) + + +### Features + +* service names ([#68](https://github.com/libp2p/js-libp2p-mdns/issues/68)) ([fa8fe22](https://github.com/libp2p/js-libp2p-mdns/commit/fa8fe22)) +* Use latest multicast-dns and dns-packet ([#69](https://github.com/libp2p/js-libp2p-mdns/issues/69)) ([cb69f2f](https://github.com/libp2p/js-libp2p-mdns/commit/cb69f2f)) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.1...v0.9.2) (2018-01-30) + + +### Bug Fixes + +* Clear interval when stopping ([#63](https://github.com/libp2p/js-libp2p-mdns/issues/63)) ([1d586c3](https://github.com/libp2p/js-libp2p-mdns/commit/1d586c3)) +* update deps for [#64](https://github.com/libp2p/js-libp2p-mdns/issues/64) ([#66](https://github.com/libp2p/js-libp2p-mdns/issues/66)) ([d4ed3b3](https://github.com/libp2p/js-libp2p-mdns/commit/d4ed3b3)) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.0...v0.9.1) (2017-09-08) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.8.0...v0.9.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#61](https://github.com/libp2p/js-libp2p-mdns/issues/61)) ([36ed2a1](https://github.com/libp2p/js-libp2p-mdns/commit/36ed2a1)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.7.1...v0.8.0) (2017-07-22) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.7.0...v0.7.1) (2017-07-09) + + +### Bug Fixes + +* support optional no options ([dd53646](https://github.com/libp2p/js-libp2p-mdns/commit/dd53646)) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.6.2...v0.7.0) (2017-03-30) + + +### Features + +* update to that new peer-info everyone is talking about ([3fd3602](https://github.com/libp2p/js-libp2p-mdns/commit/3fd3602)) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.6.1...v0.6.2) (2017-03-21) diff --git a/packages/peer-discovery-mdns/LICENSE b/packages/peer-discovery-mdns/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-discovery-mdns/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-discovery-mdns/LICENSE-APACHE b/packages/peer-discovery-mdns/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-discovery-mdns/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-discovery-mdns/LICENSE-MIT b/packages/peer-discovery-mdns/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-discovery-mdns/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-discovery-mdns/README.md b/packages/peer-discovery-mdns/README.md new file mode 100644 index 0000000000..69be7f9185 --- /dev/null +++ b/packages/peer-discovery-mdns/README.md @@ -0,0 +1,113 @@ +# @libp2p/mdns + +[![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) + +> Node.js libp2p mDNS discovery implementation for peer discovery + +## Table of contents + +- [Install](#install) +- [Usage](#usage) +- [MDNS messages](#mdns-messages) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/mdns +``` + +## Usage + +```Typescript +import { mdns } from '@libp2p/mdns' + +const options = { + peerDiscovery: [ + mdns() + ] +} + +async function start () { + const libp2p = await createLibp2p(options) + + libp2p.on('peer:discovery', function (peerId) { + console.log('found peer: ', peerId.toB58String()) + }) + + await libp2p.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` + - `interval` - query interval, default 10 \* 1000 (10 seconds) + - `serviceTag` - name of the service announce , default 'ipfs.local\` + +## MDNS messages + +A query is sent to discover the IPFS nodes on the local network + +```js +{ + type: 'query', + questions: [ { name: 'ipfs.local', type: 'PTR' } ] +} +``` + +When a query is detected, each IPFS node sends an answer about itself + +```js + [ { name: 'ipfs.local', + type: 'PTR', + class: 'IN', + ttl: 120, + data: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local' }, + { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local', + type: 'SRV', + class: 'IN', + ttl: 120, + data: + { priority: 10, + weight: 1, + port: '20002', + target: 'LAPTOP-G5LJ7VN9' } }, + { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local', + type: 'TXT', + class: 'IN', + ttl: 120, + data: ['QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK'] }, + { name: 'LAPTOP-G5LJ7VN9', + type: 'A', + class: 'IN', + ttl: 120, + data: '127.0.0.1' }, + { name: 'LAPTOP-G5LJ7VN9', + type: 'AAAA', + class: 'IN', + ttl: 120, + data: '::1' } ] +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-discovery-mdns/package.json b/packages/peer-discovery-mdns/package.json new file mode 100644 index 0000000000..7c2969d741 --- /dev/null +++ b/packages/peer-discovery-mdns/package.json @@ -0,0 +1,69 @@ +{ + "name": "@libp2p/mdns", + "version": "8.0.0", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t node", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-discovery": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "@types/multicast-dns": "^7.2.1", + "dns-packet": "^5.4.0", + "multicast-dns": "^7.2.5" + }, + "devDependencies": { + "@libp2p/interface-address-manager": "^3.0.0", + "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "aegir": "^39.0.10", + "p-wait-for": "^5.0.0", + "ts-sinon": "^2.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-discovery-mdns/src/index.ts b/packages/peer-discovery-mdns/src/index.ts new file mode 100644 index 0000000000..1d49e45555 --- /dev/null +++ b/packages/peer-discovery-mdns/src/index.ts @@ -0,0 +1,175 @@ +import { peerDiscovery } from '@libp2p/interface-peer-discovery' +import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import multicastDNS from 'multicast-dns' +import * as query from './query.js' +import { stringGen } from './utils.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import type { PeerInfo } from '@libp2p/interface-peer-info' + +const log = logger('libp2p:mdns') + +export interface MulticastDNSInit { + broadcast?: boolean + interval?: number + serviceTag?: string + peerName?: string + port?: number + ip?: string +} + +export interface MulticastDNSComponents { + addressManager: AddressManager +} + +class MulticastDNS extends EventEmitter implements PeerDiscovery { + public mdns?: multicastDNS.MulticastDNS + + private readonly broadcast: boolean + private readonly interval: number + private readonly serviceTag: string + private readonly peerName: string + private readonly port: number + private readonly ip: string + private _queryInterval: ReturnType | null + private readonly components: MulticastDNSComponents + + constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { + super() + + this.broadcast = init.broadcast !== false + this.interval = init.interval ?? (1e3 * 10) + this.serviceTag = init.serviceTag ?? '_p2p._udp.local' + this.ip = init.ip ?? '224.0.0.251' + this.peerName = init.peerName ?? stringGen(63) + // 63 is dns label limit + if (this.peerName.length >= 64) { + throw new Error('Peer name should be less than 64 chars long') + } + 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) + } + + readonly [peerDiscovery] = this + + readonly [Symbol.toStringTag] = '@libp2p/mdns' + + isStarted (): boolean { + return Boolean(this.mdns) + } + + /** + * Start sending queries to the LAN. + * + * @returns {void} + */ + async start (): Promise { + if (this.mdns != null) { + return + } + + this.mdns = multicastDNS({ port: this.port, ip: this.ip }) + this.mdns.on('query', this._onMdnsQuery) + this.mdns.on('response', this._onMdnsResponse) + + this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) + } + + _onMdnsQuery (event: multicastDNS.QueryPacket): void { + if (this.mdns == null) { + return + } + + log.trace('received incoming mDNS query') + query.gotQuery( + event, + this.mdns, + this.peerName, + this.components.addressManager.getAddresses(), + this.serviceTag, + this.broadcast) + } + + _onMdnsResponse (event: multicastDNS.ResponsePacket): void { + log.trace('received mDNS query response') + + try { + const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) + + if (foundPeer != null) { + log('discovered peer in mDNS query response %p', foundPeer.id) + + this.dispatchEvent(new CustomEvent('peer', { + detail: foundPeer + })) + } + } catch (err) { + log.error('Error processing peer response', err) + } + } + + _onPeer (evt: CustomEvent): void { + if (this.mdns == null) { + return + } + + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + } + + /** + * Stop sending queries to the LAN. + * + * @returns {Promise} + */ + async stop (): Promise { + if (this.mdns == null) { + return + } + + this.mdns.removeListener('query', this._onMdnsQuery) + this.mdns.removeListener('response', this._onMdnsResponse) + + if (this._queryInterval != null) { + clearInterval(this._queryInterval) + this._queryInterval = null + } + + await new Promise((resolve) => { + if (this.mdns != null) { + this.mdns.destroy(resolve) + } else { + resolve() + } + }) + + this.mdns = undefined + } +} + +export function mdns (init: MulticastDNSInit = {}): (components: MulticastDNSComponents) => PeerDiscovery { + return (components: MulticastDNSComponents) => new MulticastDNS(components, init) +} + +/* for reference + + [ { name: '_p2p._udp.local', + type: 'PTR', + class: 1, + ttl: 120, + data: 'XQxZeAH6MX2n4255fzYmyUCUdhQ0DAWv.p2p._udp.local' }, + + { name: 'XQxZeAH6MX2n4255fzYmyUCUdhQ0DAWv.p2p._udp.local', + type: 'TXT', + class: 1, + ttl: 120, + data: 'dnsaddr=/ip4/127.0.0.1/tcp/80/p2p/QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' }, +] + +*/ diff --git a/packages/peer-discovery-mdns/src/query.ts b/packages/peer-discovery-mdns/src/query.ts new file mode 100644 index 0000000000..da0e854087 --- /dev/null +++ b/packages/peer-discovery-mdns/src/query.ts @@ -0,0 +1,111 @@ +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr, type Multiaddr } 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' + +const log = logger('libp2p:mdns:query') + +export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: number): NodeJS.Timer { + const query = (): void => { + log('query', serviceTag) + + mdns.query({ + questions: [{ + name: serviceTag, + type: 'PTR' + }] + }) + } + + // Immediately start a query, then do it every interval. + query() + return setInterval(query, interval) +} + +export function gotResponse (rsp: ResponsePacket, localPeerName: string, serviceTag: string): PeerInfo | undefined { + if (rsp.answers == null) { + return + } + + let answerPTR: StringAnswer | undefined + const txtAnswers: TxtAnswer[] = [] + + rsp.answers.forEach((answer) => { + switch (answer.type) { + case 'PTR': answerPTR = answer; break + case 'TXT': txtAnswers.push(answer); break + default: break + } + }) + + if (answerPTR == null || + answerPTR?.name !== serviceTag || + txtAnswers.length === 0 || + answerPTR.data.startsWith(localPeerName)) { + return + } + + try { + const multiaddrs: Multiaddr[] = txtAnswers + .flatMap((a) => a.data) + .filter(answerData => answerData.toString().startsWith('dnsaddr=')) + .map((answerData) => { + return multiaddr(answerData.toString().substring('dnsaddr='.length)) + }) + + const peerId = multiaddrs[0].getPeerId() + if (peerId == null) { + throw new Error("Multiaddr doesn't contain PeerId") + } + log('peer found %p', peerId) + + return { + id: peerIdFromString(peerId), + multiaddrs, + protocols: [] + } + } catch (e) { + log.error('failed to parse mdns response', e) + } +} + +export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerName: string, multiaddrs: Multiaddr[], serviceTag: string, broadcast: boolean): void { + if (!broadcast) { + log('not responding to mDNS query as broadcast mode is false') + return + } + + if (multiaddrs.length === 0) { + return + } + + if (qry.questions[0] != null && qry.questions[0].name === serviceTag) { + const answers: Answer[] = [] + + answers.push({ + name: serviceTag, + type: 'PTR', + class: 'IN', + ttl: 120, + data: peerName + '.' + serviceTag + }) + + multiaddrs.forEach((addr) => { + // spec mandates multiaddr contains peer id + if (addr.getPeerId() != null) { + answers.push({ + name: peerName + '.' + serviceTag, + type: 'TXT', + class: 'IN', + ttl: 120, + data: 'dnsaddr=' + addr.toString() + }) + } + }) + + log('responding to query') + mdns.respond(answers) + } +} diff --git a/packages/peer-discovery-mdns/src/utils.ts b/packages/peer-discovery-mdns/src/utils.ts new file mode 100644 index 0000000000..c88c951102 --- /dev/null +++ b/packages/peer-discovery-mdns/src/utils.ts @@ -0,0 +1,9 @@ +export function stringGen (len: number): string { + let text = '' + + const charset = 'abcdefghijklmnopqrstuvwxyz0123456789' + + for (let i = 0; i < len; i++) { text += charset.charAt(Math.floor(Math.random() * charset.length)) } + + return text +} diff --git a/packages/peer-discovery-mdns/test/compliance.spec.ts b/packages/peer-discovery-mdns/test/compliance.spec.ts new file mode 100644 index 0000000000..e9b00b62f9 --- /dev/null +++ b/packages/peer-discovery-mdns/test/compliance.spec.ts @@ -0,0 +1,53 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-peer-discovery-compliance-tests' +import { CustomEvent } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { stubInterface } from 'ts-sinon' +import { mdns } from '../src/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerDiscovery } from '@libp2p/interface-peer-discovery' + +let discovery: PeerDiscovery + +describe('compliance tests', () => { + let intervalId: ReturnType + + tests({ + async setup () { + const peerId1 = await createEd25519PeerId() + const peerId2 = await createEd25519PeerId() + + const addressManager = stubInterface() + addressManager.getAddresses.returns([ + multiaddr(`/ip4/127.0.0.1/tcp/13921/p2p/${peerId1.toString()}`) + ]) + + discovery = mdns({ + broadcast: false, + port: 50001 + })({ + addressManager + }) + + // 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: [] + } + })), 1000) + + return discovery + }, + async teardown () { + clearInterval(intervalId) + await new Promise(resolve => setTimeout(resolve, 10)) + } + }) +}) diff --git a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts new file mode 100644 index 0000000000..62613717e3 --- /dev/null +++ b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts @@ -0,0 +1,222 @@ +/* eslint-env mocha */ + +import { start, stop } from '@libp2p/interfaces/startable' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import pWaitFor from 'p-wait-for' +import { stubInterface } from 'ts-sinon' +import { mdns, type MulticastDNSComponents } from './../src/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Multiaddr } from '@multiformats/multiaddr' + +function getComponents (peerId: PeerId, multiaddrs: Multiaddr[]): MulticastDNSComponents { + const addressManager = stubInterface() + addressManager.getAddresses.returns(multiaddrs.map(ma => ma.encapsulate(`/p2p/${peerId.toString()}`))) + + return { addressManager } +} + +describe('MulticastDNS', () => { + let pA: PeerId + let aMultiaddrs: Multiaddr[] + let pB: PeerId + let bMultiaddrs: Multiaddr[] + let cMultiaddrs: Multiaddr[] + let pD: PeerId + let dMultiaddrs: Multiaddr[] + + before(async function () { + this.timeout(80 * 1000) + + ;[pA, pB, pD] = await Promise.all([ + createEd25519PeerId(), + createEd25519PeerId(), + createEd25519PeerId() + ]) + + aMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/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('/dnsaddr/discovery.libp2p.io') + ] + + cMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/20003'), + multiaddr('/ip4/127.0.0.1/tcp/30003/ws'), + multiaddr('/dns4/discovery.libp2p.io') + ] + + dMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/30003/ws') + ] + }) + + it('find another peer', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + broadcast: false, // do not talk to ourself + port: 50001 + })(getComponents(pA, aMultiaddrs)) + + const mdnsB = mdns({ + port: 50001 // port must be the same + })(getComponents(pB, bMultiaddrs)) + + await start(mdnsA, mdnsB) + + const { detail: { id } } = await new Promise>((resolve) => { + mdnsA.addEventListener('peer', resolve, { + once: true + }) + }) + + expect(pB.toString()).to.eql(id.toString()) + + await stop(mdnsA, mdnsB) + }) + + it('announces all multiaddresses', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + broadcast: false, // do not talk to ourself + port: 50003 + })(getComponents(pA, aMultiaddrs)) + const mdnsB = mdns({ + port: 50003 // port must be the same + })(getComponents(pB, cMultiaddrs)) + const mdnsD = mdns({ + port: 50003 // port must be the same + })(getComponents(pD, dMultiaddrs)) + + await start(mdnsA, mdnsB, mdnsD) + + const peers = new Map() + const expectedPeer = pB.toString() + + const foundPeer = (evt: CustomEvent): Map => peers.set(evt.detail.id.toString(), evt.detail) + mdnsA.addEventListener('peer', foundPeer) + + await pWaitFor(() => peers.has(expectedPeer)) + mdnsA.removeEventListener('peer', foundPeer) + + expect(peers.get(expectedPeer).multiaddrs.length).to.equal(3) + + await stop(mdnsA, mdnsB, mdnsD) + }) + + it('doesn\'t emit peers after stop', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + port: 50004 // port must be the same + })(getComponents(pA, aMultiaddrs)) + + const mdnsC = mdns({ + port: 50004 + })(getComponents(pD, dMultiaddrs)) + + await start(mdnsA) + await new Promise((resolve) => setTimeout(resolve, 1000)) + await stop(mdnsA) + await start(mdnsC) + + mdnsC.addEventListener('peer', () => { + throw new Error('Should not receive new peer.') + }, { + once: true + }) + + await new Promise((resolve) => setTimeout(resolve, 5000)) + 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 + })(getComponents(pA, aMultiaddrs)) + await start(mdnsA) + + await new Promise((resolve, reject) => { + mdnsA.addEventListener('peer', (evt) => { + if (evt.detail == null) { + reject(new Error('peerData was not set')) + } + }) + + // @ts-expect-error not a PeerDiscovery field + if (mdnsA.mdns == null) { + reject(new Error('mdns property was not set')) + return + } + + // @ts-expect-error not a PeerDiscovery field + mdnsA.mdns.on('response', () => { + // query.gotResponse is async - we'll bail from that method when + // comparing the senders PeerId to our own but it'll happen later + // so allow enough time for the test to have failed if we emit + // empty peerData objects + setTimeout(() => { + resolve() + }, 100) + }) + + // this will cause us to respond to ourselves + // @ts-expect-error not a PeerDiscovery field + mdnsA.mdns.query({ + questions: [{ + name: 'localhost', + type: 'A' + }] + }) + }) + + await stop(mdnsA) + }) + + it('find another peer with different udp4 address', async function () { + this.timeout(40 * 1000) + + 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, bMultiaddrs)) + + await start(mdnsA, mdnsB) + + const { detail: { id } } = await new Promise>((resolve) => { + mdnsA.addEventListener('peer', resolve, { + once: true + }) + }) + + expect(pB.toString()).to.eql(id.toString()) + + await stop(mdnsA, mdnsB) + }) +}) diff --git a/packages/peer-discovery-mdns/tsconfig.json b/packages/peer-discovery-mdns/tsconfig.json new file mode 100644 index 0000000000..f89978dd99 --- /dev/null +++ b/packages/peer-discovery-mdns/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-address-manager" + }, + { + "path": "../interface-peer-discovery" + }, + { + "path": "../interface-peer-discovery-compliance-tests" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-info" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/peer-id-factory/CHANGELOG.md b/packages/peer-id-factory/CHANGELOG.md new file mode 100644 index 0000000000..c674ac0dac --- /dev/null +++ b/packages/peer-id-factory/CHANGELOG.md @@ -0,0 +1,214 @@ +## [@libp2p/peer-id-factory-v2.0.3](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v2.0.2...@libp2p/peer-id-factory-v2.0.3) (2023-03-20) + + +### Documentation + +* 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)) + +## [@libp2p/peer-id-factory-v2.0.2](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v2.0.1...@libp2p/peer-id-factory-v2.0.2) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#49](https://github.com/libp2p/js-libp2p-peer-id/issues/49)) ([48037ee](https://github.com/libp2p/js-libp2p-peer-id/commit/48037ee53d4c07a3750bbeeb40727cbaf3c23946)) + +## [@libp2p/peer-id-factory-v2.0.1](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v2.0.0...@libp2p/peer-id-factory-v2.0.1) (2023-01-18) + + +### Dependencies + +* bump aegir from 37.12.1 to 38.1.0 ([#46](https://github.com/libp2p/js-libp2p-peer-id/issues/46)) ([ba54f6a](https://github.com/libp2p/js-libp2p-peer-id/commit/ba54f6a4a35de20528d4c60a2a532c553b9a9a34)) + +## [@libp2p/peer-id-factory-v2.0.0](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.20...@libp2p/peer-id-factory-v2.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 (#41) + +### Trivial Changes + +* remove lerna ([#43](https://github.com/libp2p/js-libp2p-peer-id/issues/43)) ([d458051](https://github.com/libp2p/js-libp2p-peer-id/commit/d458051bfcb7ff83c42ed26e1c12ac3d07bee492)) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 ([#41](https://github.com/libp2p/js-libp2p-peer-id/issues/41)) ([2aa0f79](https://github.com/libp2p/js-libp2p-peer-id/commit/2aa0f799789b52758651c115b3a021ca67e5c407)) +* update sibling dependencies ([fa59f28](https://github.com/libp2p/js-libp2p-peer-id/commit/fa59f289f543ecafa5c763cb0dba08eb0c24e8d8)) + +## [@libp2p/peer-id-factory-v1.0.20](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.19...@libp2p/peer-id-factory-v1.0.20) (2022-12-16) + + +### Documentation + +* publish api docs for all modules ([#39](https://github.com/libp2p/js-libp2p-peer-id/issues/39)) ([861957a](https://github.com/libp2p/js-libp2p-peer-id/commit/861957add8610498bf095d82dd51af0082eae4b5)) + +## [@libp2p/peer-id-factory-v1.0.19](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.18...@libp2p/peer-id-factory-v1.0.19) (2022-10-12) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#28](https://github.com/libp2p/js-libp2p-peer-id/issues/28)) ([e270265](https://github.com/libp2p/js-libp2p-peer-id/commit/e27026508b3684e6cb2eb896de19c161dbd21d45)) + +## [@libp2p/peer-id-factory-v1.0.18](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.17...@libp2p/peer-id-factory-v1.0.18) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#22](https://github.com/libp2p/js-libp2p-peer-id/issues/22)) ([f86d87a](https://github.com/libp2p/js-libp2p-peer-id/commit/f86d87a547e3f1dc13c3aa9816770964830fda6d)) + +## [@libp2p/peer-id-factory-v1.0.17](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.16...@libp2p/peer-id-factory-v1.0.17) (2022-07-31) + + +### Trivial Changes + +* update project config ([#14](https://github.com/libp2p/js-libp2p-peer-id/issues/14)) ([5c3918c](https://github.com/libp2p/js-libp2p-peer-id/commit/5c3918c61d8346ed1d49094bb592f8c872b7de57)) + + +### Dependencies + +* update protons and uint8arraylist ([#17](https://github.com/libp2p/js-libp2p-peer-id/issues/17)) ([90588f5](https://github.com/libp2p/js-libp2p-peer-id/commit/90588f599fe9522a0ccfd43f1405425031c5175d)) + +## [@libp2p/peer-id-factory-v1.0.16](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.15...@libp2p/peer-id-factory-v1.0.16) (2022-07-18) + + +### Trivial Changes + +* update sibling dependencies [skip ci] ([4ecbbe0](https://github.com/libp2p/js-libp2p-peer-id/commit/4ecbbe0247dd664a172008ff9255f0f79c04ffb9)) + +## [@libp2p/peer-id-factory-v1.0.15](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.14...@libp2p/peer-id-factory-v1.0.15) (2022-06-17) + + +### Trivial Changes + +* **deps:** bump @libp2p/interface-keys from 0.0.1 to 1.0.2 ([#4](https://github.com/libp2p/js-libp2p-peer-id/issues/4)) ([8aff9ce](https://github.com/libp2p/js-libp2p-peer-id/commit/8aff9ce5b8556f438fd2b7389f8070ae7d217a84)) + +## [@libp2p/peer-id-factory-v1.0.14](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.13...@libp2p/peer-id-factory-v1.0.14) (2022-06-17) + + +### Trivial Changes + +* **deps:** bump @libp2p/crypto from 0.22.14 to 1.0.0 ([#3](https://github.com/libp2p/js-libp2p-peer-id/issues/3)) ([21ff018](https://github.com/libp2p/js-libp2p-peer-id/commit/21ff018a634f58bdd4b9b943e84ae03b1f1ef69d)) + +## [@libp2p/peer-id-factory-v1.0.13](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.12...@libp2p/peer-id-factory-v1.0.13) (2022-06-14) + + +### Trivial Changes + +* **deps:** bump @libp2p/interface-peer-id from 0.0.1 to 1.0.0 ([#2](https://github.com/libp2p/js-libp2p-peer-id/issues/2)) ([69c0c49](https://github.com/libp2p/js-libp2p-peer-id/commit/69c0c495ab04d5b97de27d3ef20e1ee78d5b0056)) + +## [@libp2p/peer-id-factory-v1.0.12](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.11...@libp2p/peer-id-factory-v1.0.12) (2022-06-10) + + +### Trivial Changes + +* update interface deps ([#1](https://github.com/libp2p/js-libp2p-peer-id/issues/1)) ([3cf652d](https://github.com/libp2p/js-libp2p-peer-id/commit/3cf652d50ede0d876da46dcb0b1de387126e272a)) + +## [@libp2p/peer-id-factory-v1.0.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.10...@libp2p/peer-id-factory-v1.0.11) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-id-factory-v1.0.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.9...@libp2p/peer-id-factory-v1.0.10) (2022-05-10) + + +### Bug Fixes + +* regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) + +## [@libp2p/peer-id-factory-v1.0.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.8...@libp2p/peer-id-factory-v1.0.9) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-id-factory-v1.0.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.7...@libp2p/peer-id-factory-v1.0.8) (2022-03-15) + + +### 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)) + +## [@libp2p/peer-id-factory-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.6...@libp2p/peer-id-factory-v1.0.7) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-id-factory-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.5...@libp2p/peer-id-factory-v1.0.6) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-id-factory-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.4...@libp2p/peer-id-factory-v1.0.5) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-id-factory-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.3...@libp2p/peer-id-factory-v1.0.4) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/peer-id-factory-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.2...@libp2p/peer-id-factory-v1.0.3) (2022-01-15) + + +### Trivial Changes + +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/peer-id-factory-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.1...@libp2p/peer-id-factory-v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) +* update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.2.0 (2022-01-04) + + +### Features + +* add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) +* update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) + + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-peer-id-factory@0.2.0...libp2p-peer-id-factory@0.2.1) (2022-01-02) + +**Note:** Version bump only for package libp2p-peer-id-factory + + + + + +# 0.2.0 (2022-01-02) + + +### Features + +* 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/LICENSE b/packages/peer-id-factory/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-id-factory/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-id-factory/LICENSE-APACHE b/packages/peer-id-factory/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-id-factory/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-id-factory/LICENSE-MIT b/packages/peer-id-factory/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-id-factory/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-id-factory/README.md b/packages/peer-id-factory/README.md new file mode 100644 index 0000000000..7c353cd96b --- /dev/null +++ b/packages/peer-id-factory/README.md @@ -0,0 +1,68 @@ +# @libp2p/peer-id-factory + +[![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) + +> Create PeerId instances + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +Generate, import, and export PeerIDs, for use with [IPFS](https://github.com/ipfs/ipfs). + +A Peer ID is the SHA-256 [multihash](https://github.com/multiformats/multihash) of a public key. + +The public key is a base64 encoded string of a protobuf containing an RSA DER buffer. This uses a node buffer to pass the base64 encoded public key protobuf to the multihash for ID generation. + +## Example + +```JavaScript +import { createEd25519PeerId } from '@libp2p/peer-id-factory' + +const peerId = await createEd25519PeerId() +console.log(id.toString()) +``` + +```bash +12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-id-factory/package.json b/packages/peer-id-factory/package.json new file mode 100644 index 0000000000..8d03095bc8 --- /dev/null +++ b/packages/peer-id-factory/package.json @@ -0,0 +1,71 @@ +{ + "name": "@libp2p/peer-id-factory", + "version": "2.0.3", + "description": "Create PeerId instances", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-id-factory#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "proto.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "generate": "protons src/proto.proto", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-keys": "^1.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "multiformats": "^11.0.2", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "aegir": "^39.0.10", + "protons": "^7.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-id-factory/src/index.ts b/packages/peer-id-factory/src/index.ts new file mode 100644 index 0000000000..f3b66da887 --- /dev/null +++ b/packages/peer-id-factory/src/index.ts @@ -0,0 +1,91 @@ +import { generateKeyPair, marshalPrivateKey, unmarshalPrivateKey, marshalPublicKey, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { peerIdFromKeys, peerIdFromBytes } from '@libp2p/peer-id' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PeerIdProto } from './proto.js' +import type { PublicKey, PrivateKey } from '@libp2p/interface-keys' +import type { RSAPeerId, Ed25519PeerId, Secp256k1PeerId, PeerId } from '@libp2p/interface-peer-id' + +export const createEd25519PeerId = async (): Promise => { + const key = await generateKeyPair('Ed25519') + const id = await createFromPrivKey(key) + + if (id.type === 'Ed25519') { + return id + } + + throw new Error(`Generated unexpected PeerId type "${id.type}"`) +} + +export const createSecp256k1PeerId = async (): Promise => { + const key = await generateKeyPair('secp256k1') + const id = await createFromPrivKey(key) + + if (id.type === 'secp256k1') { + return id + } + + throw new Error(`Generated unexpected PeerId type "${id.type}"`) +} + +export const createRSAPeerId = async (opts?: { bits: number }): Promise => { + const key = await generateKeyPair('RSA', opts?.bits ?? 2048) + const id = await createFromPrivKey(key) + + if (id.type === 'RSA') { + return id + } + + throw new Error(`Generated unexpected PeerId type "${id.type}"`) +} + +export async function createFromPubKey (publicKey: PublicKey): Promise { + return peerIdFromKeys(marshalPublicKey(publicKey)) +} + +export async function createFromPrivKey (privateKey: PrivateKey): Promise { + return peerIdFromKeys(marshalPublicKey(privateKey.public), marshalPrivateKey(privateKey)) +} + +export function exportToProtobuf (peerId: RSAPeerId | Ed25519PeerId | Secp256k1PeerId, excludePrivateKey?: boolean): Uint8Array { + return PeerIdProto.encode({ + id: peerId.multihash.bytes, + pubKey: peerId.publicKey, + privKey: excludePrivateKey === true || peerId.privateKey == null ? undefined : peerId.privateKey + }) +} + +export async function createFromProtobuf (buf: Uint8Array): Promise { + const { + id, + privKey, + pubKey + } = PeerIdProto.decode(buf) + + return createFromParts( + id ?? new Uint8Array(0), + privKey, + pubKey + ) +} + +export async function createFromJSON (obj: { id: string, privKey?: string, pubKey?: string }): Promise { + return createFromParts( + uint8ArrayFromString(obj.id, 'base58btc'), + obj.privKey != null ? uint8ArrayFromString(obj.privKey, 'base64pad') : undefined, + obj.pubKey != null ? uint8ArrayFromString(obj.pubKey, 'base64pad') : undefined + ) +} + +async function createFromParts (multihash: Uint8Array, privKey?: Uint8Array, pubKey?: Uint8Array): Promise { + if (privKey != null) { + const key = await unmarshalPrivateKey(privKey) + + return createFromPrivKey(key) + } else if (pubKey != null) { + const key = unmarshalPublicKey(pubKey) + + return createFromPubKey(key) + } + + return peerIdFromBytes(multihash) +} diff --git a/packages/peer-id-factory/src/proto.proto b/packages/peer-id-factory/src/proto.proto new file mode 100644 index 0000000000..39f8868c76 --- /dev/null +++ b/packages/peer-id-factory/src/proto.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message PeerIdProto { + optional bytes id = 1; + optional bytes pubKey = 2; + optional bytes privKey = 3; +} diff --git a/packages/peer-id-factory/src/proto.ts b/packages/peer-id-factory/src/proto.ts new file mode 100644 index 0000000000..bcd0a25d21 --- /dev/null +++ b/packages/peer-id-factory/src/proto.ts @@ -0,0 +1,83 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface PeerIdProto { + id?: Uint8Array + pubKey?: Uint8Array + privKey?: Uint8Array +} + +export namespace PeerIdProto { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.id != null) { + w.uint32(10) + w.bytes(obj.id) + } + + if (obj.pubKey != null) { + w.uint32(18) + w.bytes(obj.pubKey) + } + + if (obj.privKey != null) { + w.uint32(26) + w.bytes(obj.privKey) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.id = reader.bytes() + break + case 2: + obj.pubKey = reader.bytes() + break + case 3: + obj.privKey = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PeerIdProto.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PeerIdProto => { + return decodeMessage(buf, PeerIdProto.codec()) + } +} diff --git a/packages/peer-id-factory/test/fixtures/go-private-key.ts b/packages/peer-id-factory/test/fixtures/go-private-key.ts new file mode 100644 index 0000000000..5acd665de1 --- /dev/null +++ b/packages/peer-id-factory/test/fixtures/go-private-key.ts @@ -0,0 +1,5 @@ + +export default { + id: 'QmRLoXS3E73psYaUsma1VSbboTa2J8Z9kso1tpiGLk9WQ4', + privKey: 'CAASpwkwggSjAgEAAoIBAQDWBEbO8kc6a5kEks09CKPQargY3p0DCmCczoCT52/RYFqxvH9dI+s+u4ZAvF9aLWOBvFomL7jHZODPxKDrbiNCmyEbViNgZYK+PNbwh0V3ZGbB27X3q8yZtLvYA8dhcNkz/2SHBarSoC4QLA5MXUuSWtVaYMY3MzMnzBF57Jc9Ase7NvHOIUI90M7aN5izP7hxPXpZ+shiN+TyjM8mFxYONG7ZSsY3IxUhtrU5MRzFX+tp1o/gb/aa51mHf7AL3N02j5ABiYbCK97Rbwr03hsBcwgMxoDPJmP3WZ+D5yyPcOIIF1Vd7+4/f7FQJnIw3xr9/jvaFbPyDCVbBOhr9oyxAgMBAAECggEALlrgx2Q8v0+c5hux7p1XdgYXd/OHyKfPw0cLHH4NfylCm6q7X34vLvhJHO5wLMUV/3y/ffPqLu4Pr5DkVfoWExAsvJIMuY1jIzdkStbR2glaJHUlVc7VUxmNcj1nSxi5QwT3TjORC2v8bi5Mroeqnbmk6p15cW1akC0oP+NZ4rG48+WFHRqsBaBusdSOVfA+IiZUqSd1ILysJ1w7aVN3EC7jLjDG43i+P/2BcEHy8TVClGOknJL341bHe3UPdEpmeu6k6aHGlDI4blUMXahCIUh0IdZuj+Vi/TxQME9+3bKIOjQb8RCNm3U3j/uz5gs9SyTjBuYIib9Scj/jDbLh0QKBgQDfLr3go3Q/AR0jb12QjGALJz1lc9ZRX2RQJkqqmYkZwOlHHyl+YJgqOZiO80fUkN0sJ29CmKecXU4gXuHir913Fdceei1ScBSsvZpWtBLhEZXKrRJYq8U0atKUFQADDMGutyB/uGCNeNwR6VcJezHPICvHxQfmWlWHA5VIOEtRPQKBgQD1fID76SkIpF/EaJMnN2alXWWnzKhUBUPGpQtbpwgSfaCBiZ4vr3NQwKBntOOB5QwHmifNZMoqaFQLzC4B/uyTNUcQMQQ6arYav7WQXqXTmW6poTsjUSuSOPx1swsHlYX09SmUwWDfd94XF9UOU0KUfA2/c85ixzNlV5ejkFA4hQKBgEvP3uQN4hD82d8Nl2TgqkdfnvV1cdnWY4buWvK0kOPUqelk5n1tZoMBaZc1gLLuOpMjGiIvJNByyXUpheWxA7POEXLi4b5dIEjFZ0YIiVk21gEw5UiFoMl7d+ihcY2Xqbslrb507SdhZLAY6V3pITRQo06K2XIgQWlJiE4uATepAoGBALZ2vEiBnYZW5vfN4tKbUyhGq3B1pggNgbr8odyV4mIcDlk6OOGov0WeZ5ut0AyUesSLyFnaOIoc0ZuTP/8rxBwG1bMrO8FP39sx83pDX25P9PkQZixyALjGsp+pXOFeOhtAvo9azO5M4j638Bydtjc3neBX62dwOLtyx7tDYN0hAoGAVLmr3w7XMVHTfEuCSzKHyRrOaN2PAuSX31QAji1PwlwVKMylVrb8rRvBOpTicA/wXPX9Q5O/yjegqhqLT/LXAm9ziFzy5b9/9SzXPukKebXXbvc0FOmcsrcxtijlPyUzf9fKM1ShiwqqsgM9eNyZ9GWUJw2GFATCWW7pl7rtnWk=' +} diff --git a/packages/peer-id-factory/test/fixtures/sample-id.ts b/packages/peer-id-factory/test/fixtures/sample-id.ts new file mode 100644 index 0000000000..79f0645c85 --- /dev/null +++ b/packages/peer-id-factory/test/fixtures/sample-id.ts @@ -0,0 +1,7 @@ + +export default { + id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', + privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=', + marshalled: '0a22122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a912ab02080012a60230820122300d06092a864886f70d01010105000382010f003082010a0282010100b648aa3f1cc1597819a5d401775e28f3af1adf417749ce378f05901b771a8a47531cea3b911d78a3e875d83e3940934d41845d52dcb9782f08b47001e18207f8e7bb0c839e545b278629e52fd2e720bc2a41c25479710d36d22d0c8338cf58e2d6ab5aedbd26cd7008b6644567ebe43611c1e8df052f591b4b78acfe0d94997f0d8f1030be0c63c93e5edff20ef3979e98ca69a6cc7f658992cdaf383faa2768914bf9bb5a5d1ab7292ee3cd79338393472a281f8e51bb8a8fd1928581020848dac9b24397ddbbea86a52fd82106d49e12fdb492e81ab53bd8cb9f74c05949924bf297e9cfc481f410460c28af5745696ef57627a127dba22c1cbfc3374a5b2302030100011aab09080012a609308204a20201000282010100b648aa3f1cc1597819a5d401775e28f3af1adf417749ce378f05901b771a8a47531cea3b911d78a3e875d83e3940934d41845d52dcb9782f08b47001e18207f8e7bb0c839e545b278629e52fd2e720bc2a41c25479710d36d22d0c8338cf58e2d6ab5aedbd26cd7008b6644567ebe43611c1e8df052f591b4b78acfe0d94997f0d8f1030be0c63c93e5edff20ef3979e98ca69a6cc7f658992cdaf383faa2768914bf9bb5a5d1ab7292ee3cd79338393472a281f8e51bb8a8fd1928581020848dac9b24397ddbbea86a52fd82106d49e12fdb492e81ab53bd8cb9f74c05949924bf297e9cfc481f410460c28af5745696ef57627a127dba22c1cbfc3374a5b2302030100010282010066d8eefdb70abca14fcf49a41e2689729c9ccbd4932a9868ae9093f37b2b055422e7d09d154e8c8fe68bff1b749023cc562809c3c3f7fd808427d27ead2f01b28584fb159412c26fb57a13eefccf1da02d337722d4765ddf4d8ccf5f86812f04a5dc7eec5e69f345c014b0d49c42f33b329fb6f58666659f49e0e7b25c1538d90bff5540cf02b2ec27ba864e12c5113b976344d8e9254873b30865357fbf19cd560a4a74b9020f58ac68ce0264ce5c36ca34a37fa88a2b010d5ba2fcc6a02c31de21886ad40a14ec72542c8ed4fb09613ec93be9196e105645113e2fb97ea693c447d6dd2c5c6cd6de42aca734efc87ec2e52bd394b53f52635e4ebca64dfe9102818100de2bc011d75dfbdccce26fabb3a631b380d44ccdd60db84c568f1cb1033cf9dcd011ef3acf1ef5ef7c8aa30d270b27835c44ed9375d85701f66838f547e64e0f24728b04f2ae5d9a56968a24080c84358efe3dde794bcafe6be32eb2b31a8183658dbe566d54e037c7207698a6f656db20596937a4996958cb40bdc9f13587eb02818100d20a1cc8b64a965f5d4236cb49f73272504db423b2eba720493601b582dbf3dd93144029f73f1c47b50ccdf67d4fd2649262cfa304a3eea12c982edd70c1ed74fe5a602f8ae4296537fe6d4ccadd2dbde27d59ca8787ab737006dbfdf5e95054ffa384960e299690f92e09bfbc8ffff6ca25e4d1afd3d9fdfacca32e66fba3a90281802e81ec10100c6d87d81fe28e87e9d767a3254dfa9cbf7c800672a8e7e92c9f8578ccf84e504343ea6120c8671d70395247436a943ecc0dd2ac593eeb21a4f55c381dfe3a07ef364af3ab49b9a731af8f62a29822f533478820df8acbffb021c276c4c83e615eae1d1f030db080eafa5d9e94f8f09bf53d57481d025dbeaf9d070281802edb0aa8cbe1bfc1ee7003013eb2e29215cfffcba6f2630a14caf37ea67ea2dc5f1f39612342f4f01a378d0adbd19ec1c8d63a33c7a93a66c22800ec6d6715adefc0018d1992e4992bf09a397357fc084c2a628987ca8038f458d362c8251042a5f4b873311d9df521615fd362214d9ca463e7b3cf619753cd4b316bfc954e610281806beec9501236f93a79f99999c60e1fbbd81c4b35d83006484ed0e09da5d212aa4d05d0fc5bcb6d8314e297644a62c88f5760fd42f303e226c4a11a6db213004f5979ebad9356733695b826d71eb664590a200431b71c65cd754e0c0160b28989728a7201a4fa68009652ce918b9966cc5a1dbcf91252e80417e8a1eb2b5a36bb' +} diff --git a/packages/peer-id-factory/test/index.spec.ts b/packages/peer-id-factory/test/index.spec.ts new file mode 100644 index 0000000000..aa32dd2574 --- /dev/null +++ b/packages/peer-id-factory/test/index.spec.ts @@ -0,0 +1,313 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import util from 'util' +import { keys } from '@libp2p/crypto' +import { peerIdFromString, peerIdFromBytes, peerIdFromCID, createPeerId } from '@libp2p/peer-id' +import { expect } from 'aegir/chai' +import { base16 } from 'multiformats/bases/base16' +import { base36 } from 'multiformats/bases/base36' +import { base58btc } from 'multiformats/bases/base58' +import { CID } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' +import { identity } from 'multiformats/hashes/identity' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import * as PeerIdFactory from '../src/index.js' +import goId from './fixtures/go-private-key.js' +import testId from './fixtures/sample-id.js' + +const LIBP2P_KEY_CODE = 0x72 +const RAW_CODE = 0x55 + +const testIdBytes = base16.decode(`f${testId.id}`) +const testIdDigest = Digest.decode(testIdBytes) +const testIdB58String = base58btc.encode(testIdBytes).substring(1) +const testIdB36String = base36.encode(testIdBytes) +const testIdCID = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) +const testIdCIDString = testIdCID.toString() + +describe('PeerId', () => { + it('create an id without \'new\'', () => { + // @ts-expect-error missing args + expect(() => createPeerId()).to.throw(Error) + }) + + it('create a new id', async () => { + const id = await PeerIdFactory.createEd25519PeerId() + expect(id.toString().length).to.equal(52) + }) + + it('can be created for a secp256k1 key', async () => { + const id = await PeerIdFactory.createSecp256k1PeerId() + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('can get the public key from a Secp256k1 key', async () => { + const original = await PeerIdFactory.createSecp256k1PeerId() + const newId = peerIdFromString(original.toString()) + expect(original.publicKey).to.equalBytes(newId.publicKey) + }) + + it('recreate from a Uint8Array', () => { + const id = peerIdFromBytes(testIdBytes) + expect(testId.id).to.equal(uint8ArrayToString(id.multihash.bytes, 'base16')) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from a B58 String', () => { + const id = peerIdFromString(testIdB58String) + expect(testIdB58String).to.equal(id.toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from CID object', () => { + const id = peerIdFromCID(testIdCID) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from Base58 String (CIDv0)', () => { + const id = peerIdFromCID(CID.parse(testIdB58String)) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from Base36 String', () => { + const id = peerIdFromString(testIdB36String) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from CIDv1 Base32 (libp2p-key multicodec)', () => { + const cid = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) + const id = peerIdFromCID(cid) + expect(cid.toString()).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from CID Uint8Array', () => { + const id = peerIdFromBytes(testIdCID.bytes) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('throws on invalid CID multicodec', () => { + // only libp2p and dag-pb are supported + const invalidCID = CID.createV1(RAW_CODE, testIdDigest) + expect(() => { + peerIdFromCID(invalidCID) + }).to.throw(/invalid/i) + }) + + it('throws on invalid multihash value', () => { + // using function code 0x50 that does not represent valid hash function + // https://github.com/multiformats/js-multihash/blob/b85999d5768bf06f1b0f16b926ef2cb6d9c14265/src/constants.js#L345 + const invalidMultihash = uint8ArrayToString(Uint8Array.from([0x50, 0x1, 0x0]), 'base58btc') + expect(() => { + peerIdFromString(invalidMultihash) + }).to.throw(/Non-base32hexpadupper character/i) + }) + + it('throws on invalid CID object', () => { + const invalidCID = {} + expect(() => { + // @ts-expect-error invalid cid is invalid type + peerIdFromCID(invalidCID) + }).to.throw(/invalid/i) + }) + + it('recreate from a Public Key', async () => { + const id = await PeerIdFactory.createFromPubKey(keys.unmarshalPublicKey(uint8ArrayFromString(testId.pubKey, 'base64pad'))) + + expect(testIdB58String).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.multihash.bytes) + }) + + it('recreate from a Private Key', async () => { + const id = await PeerIdFactory.createFromPrivKey(await keys.unmarshalPrivateKey(uint8ArrayFromString(testId.privKey, 'base64pad'))) + expect(testIdB58String).to.equal(id.toString()) + + const encoded = await keys.unmarshalPrivateKey(uint8ArrayFromString(testId.privKey, 'base64pad')) + const id2 = await PeerIdFactory.createFromPrivKey(encoded) + + if (id.type !== 'RSA') { + throw new Error('Wrong key type found') + } + + expect(testIdB58String).to.equal(id2.toString()) + expect(id.publicKey).to.deep.equal(id2.publicKey) + }) + + it('recreate from Protobuf', async () => { + const id = await PeerIdFactory.createFromProtobuf(uint8ArrayFromString(testId.marshalled, 'base16')) + expect(testIdB58String).to.equal(id.toString()) + + const key = await keys.unmarshalPrivateKey(uint8ArrayFromString(testId.privKey, 'base64pad')) + const id2 = await PeerIdFactory.createFromPrivKey(key) + + expect(testIdB58String).to.equal(id2.toString()) + expect(id.publicKey).to.equalBytes(id2.publicKey) + expect(uint8ArrayToString(PeerIdFactory.exportToProtobuf(id).subarray(), 'base16')).to.equal(testId.marshalled) + }) + + it('recreate from embedded ed25519 key', async () => { + const key = '12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD' + const id = peerIdFromString(key) + expect(id.toString()).to.equal(key) + + if (id.publicKey == null) { + throw new Error('No pubic key found on Ed25519 key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('recreate from embedded secp256k1 key', async () => { + const key = '16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E' + const id = peerIdFromString(key) + expect(id.toString()).to.equal(key) + + if (id.publicKey == null) { + throw new Error('No pubic key found on secp256k1 key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('recreate from string key', async () => { + const key = 'QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC' + const id = peerIdFromString(key) + expect(id.toString()).to.equal(key) + }) + + it('can be created from a secp256k1 public key', async () => { + const privKey = await keys.generateKeyPair('secp256k1', 256) + const id = await PeerIdFactory.createFromPubKey(privKey.public) + + if (id.publicKey == null) { + throw new Error('No public key found on peer id created from secp256k1 public key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('can be created from a Secp256k1 private key', async () => { + const privKey = await keys.generateKeyPair('secp256k1', 256) + const id = await PeerIdFactory.createFromPrivKey(privKey) + + if (id.publicKey == null) { + throw new Error('No public key found on peer id created from secp256k1 private key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('Compare generated ID with one created from PubKey', async () => { + const id1 = await PeerIdFactory.createSecp256k1PeerId() + const id2 = await PeerIdFactory.createFromPubKey(keys.unmarshalPublicKey(id1.publicKey)) + + expect(id1.multihash.bytes).to.equalBytes(id2.multihash.bytes) + }) + + it('Works with default options', async function () { + const id = await PeerIdFactory.createEd25519PeerId() + expect(id.toString().length).to.equal(52) + }) + + it('Non-default # of bits', async function () { + const shortId = await PeerIdFactory.createRSAPeerId({ bits: 512 }) + const longId = await PeerIdFactory.createRSAPeerId({ bits: 1024 }) + + if (longId.privateKey == null) { + throw new Error('No private key found on peer id') + } + + expect(shortId.privateKey).to.have.property('length').that.is.lessThan(longId.privateKey.length) + }) + + it('equals', async () => { + const ids = await Promise.all([ + PeerIdFactory.createEd25519PeerId(), + PeerIdFactory.createEd25519PeerId() + ]) + + expect(ids[0].equals(ids[0])).to.equal(true) + expect(ids[0].equals(ids[1])).to.equal(false) + expect(ids[0].equals(ids[0].multihash.bytes)).to.equal(true) + expect(ids[0].equals(ids[1].multihash.bytes)).to.equal(false) + }) + + describe('fromJSON', () => { + it('full node', async () => { + const id = await PeerIdFactory.createEd25519PeerId() + const other = await PeerIdFactory.createFromJSON({ + id: id.toString(), + privKey: id.privateKey != null ? uint8ArrayToString(id.privateKey, 'base64pad') : undefined, + pubKey: uint8ArrayToString(id.publicKey, 'base64pad') + }) + expect(id.toString()).to.equal(other.toString()) + expect(id.privateKey).to.equalBytes(other.privateKey) + expect(id.publicKey).to.equalBytes(other.publicKey) + }) + + it('only id', async () => { + const key = await keys.generateKeyPair('RSA', 1024) + const digest = await key.public.hash() + const id = peerIdFromBytes(digest) + expect(id.privateKey).to.not.exist() + expect(id.publicKey).to.not.exist() + const other = await PeerIdFactory.createFromJSON({ + id: id.toString(), + privKey: id.privateKey != null ? uint8ArrayToString(id.privateKey, 'base64pad') : undefined, + pubKey: id.publicKey != null ? uint8ArrayToString(id.publicKey, 'base64pad') : undefined + }) + expect(id.toString()).to.equal(other.toString()) + }) + + it('go interop', async () => { + const id = await PeerIdFactory.createFromJSON(goId) + expect(id.toString()).to.eql(goId.id) + }) + }) + + it('keys are equal after one is stringified', async () => { + const peerId = await PeerIdFactory.createEd25519PeerId() + const peerId1 = peerIdFromString(peerId.toString()) + const peerId2 = peerIdFromString(peerId.toString()) + + expect(peerId1).to.deep.equal(peerId2) + + peerId1.toString() + + expect(peerId1).to.deep.equal(peerId2) + }) + + describe('returns error instead of crashing', () => { + const garbage = [ + uint8ArrayFromString('00010203040506070809', 'base16'), + {}, null, false, undefined, true, 1, 0, + uint8ArrayFromString(''), 'aGVsbG93b3JsZA==', 'helloworld', '' + ] + + const fncs = ['createFromPubKey', 'createFromPrivKey', 'createFromJSON', 'createFromProtobuf'] + + for (const gb of garbage) { + for (const fn of fncs) { + it(`${fn} (${util.inspect(gb)})`, async () => { + try { + // @ts-expect-error cannot use a string to index PeerId + await PeerIdFactory[fn](gb) + } catch (err) { + expect(err).to.exist() + } + }) + } + } + }) +}) diff --git a/packages/peer-id-factory/tsconfig.json b/packages/peer-id-factory/tsconfig.json new file mode 100644 index 0000000000..6338201752 --- /dev/null +++ b/packages/peer-id-factory/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-keys" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/peer-id/CHANGELOG.md b/packages/peer-id/CHANGELOG.md new file mode 100644 index 0000000000..c2d9a92914 --- /dev/null +++ b/packages/peer-id/CHANGELOG.md @@ -0,0 +1,236 @@ +## [@libp2p/peer-id-v2.0.3](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v2.0.2...@libp2p/peer-id-v2.0.3) (2023-03-20) + + +### Documentation + +* 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)) + +## [@libp2p/peer-id-v2.0.2](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v2.0.1...@libp2p/peer-id-v2.0.2) (2023-02-23) + + +### Documentation + +* Fix example in README.md ([#54](https://github.com/libp2p/js-libp2p-peer-id/issues/54)) ([294a907](https://github.com/libp2p/js-libp2p-peer-id/commit/294a907ef7c13710045cb2224ab2398e78b353df)) + +## [@libp2p/peer-id-v2.0.1](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v2.0.0...@libp2p/peer-id-v2.0.1) (2023-01-18) + + +### Trivial Changes + +* replace err-code with CodeError ([#45](https://github.com/libp2p/js-libp2p-peer-id/issues/45)) ([06e39f0](https://github.com/libp2p/js-libp2p-peer-id/commit/06e39f0a8ca9e25cd3d055023ae61cde510183f8)), closes [#1269](https://github.com/libp2p/js-libp2p-peer-id/issues/1269) + + +### Dependencies + +* bump aegir from 37.12.1 to 38.1.0 ([#46](https://github.com/libp2p/js-libp2p-peer-id/issues/46)) ([ba54f6a](https://github.com/libp2p/js-libp2p-peer-id/commit/ba54f6a4a35de20528d4c60a2a532c553b9a9a34)) + +## [@libp2p/peer-id-v2.0.0](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.18...@libp2p/peer-id-v2.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 (#41) + +### Trivial Changes + +* remove lerna ([#43](https://github.com/libp2p/js-libp2p-peer-id/issues/43)) ([d458051](https://github.com/libp2p/js-libp2p-peer-id/commit/d458051bfcb7ff83c42ed26e1c12ac3d07bee492)) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 ([#41](https://github.com/libp2p/js-libp2p-peer-id/issues/41)) ([2aa0f79](https://github.com/libp2p/js-libp2p-peer-id/commit/2aa0f799789b52758651c115b3a021ca67e5c407)) + +## [@libp2p/peer-id-v1.1.18](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.17...@libp2p/peer-id-v1.1.18) (2022-12-16) + + +### Documentation + +* publish api docs for all modules ([#39](https://github.com/libp2p/js-libp2p-peer-id/issues/39)) ([861957a](https://github.com/libp2p/js-libp2p-peer-id/commit/861957add8610498bf095d82dd51af0082eae4b5)) + +## [@libp2p/peer-id-v1.1.17](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.16...@libp2p/peer-id-v1.1.17) (2022-12-08) + + +### Bug Fixes + +* human readable peer ids in console.log ([#36](https://github.com/libp2p/js-libp2p-peer-id/issues/36)) ([f80d1ea](https://github.com/libp2p/js-libp2p-peer-id/commit/f80d1ea0b6272692de69ed1d224e6cc16d9b84fb)) + + +### Trivial Changes + +* fix peer-d typo in readmes ([#31](https://github.com/libp2p/js-libp2p-peer-id/issues/31)) ([2276076](https://github.com/libp2p/js-libp2p-peer-id/commit/2276076650a0e1ecd0954be104eb7269e688b6ec)) + +## [@libp2p/peer-id-v1.1.16](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.15...@libp2p/peer-id-v1.1.16) (2022-10-12) + + +### Trivial Changes + +* update project config ([#14](https://github.com/libp2p/js-libp2p-peer-id/issues/14)) ([5c3918c](https://github.com/libp2p/js-libp2p-peer-id/commit/5c3918c61d8346ed1d49094bb592f8c872b7de57)) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#28](https://github.com/libp2p/js-libp2p-peer-id/issues/28)) ([e270265](https://github.com/libp2p/js-libp2p-peer-id/commit/e27026508b3684e6cb2eb896de19c161dbd21d45)) + +## [@libp2p/peer-id-v1.1.15](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.14...@libp2p/peer-id-v1.1.15) (2022-07-26) + + +### Bug Fixes + +* move error creation ([#11](https://github.com/libp2p/js-libp2p-peer-id/issues/11)) ([9d957fb](https://github.com/libp2p/js-libp2p-peer-id/commit/9d957fb141e30cf1064413cd649bb2c8724e935e)) + +## [@libp2p/peer-id-v1.1.14](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.13...@libp2p/peer-id-v1.1.14) (2022-07-18) + + +### Bug Fixes + +* Typo in constant name ([#7](https://github.com/libp2p/js-libp2p-peer-id/issues/7)) ([9063b65](https://github.com/libp2p/js-libp2p-peer-id/commit/9063b65ee3a29ca834588e915e490b9ec802647c)) + +## [@libp2p/peer-id-v1.1.13](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.12...@libp2p/peer-id-v1.1.13) (2022-06-14) + + +### Trivial Changes + +* **deps:** bump @libp2p/interface-peer-id from 0.0.1 to 1.0.0 ([#2](https://github.com/libp2p/js-libp2p-peer-id/issues/2)) ([69c0c49](https://github.com/libp2p/js-libp2p-peer-id/commit/69c0c495ab04d5b97de27d3ef20e1ee78d5b0056)) + +## [@libp2p/peer-id-v1.1.12](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.11...@libp2p/peer-id-v1.1.12) (2022-06-10) + + +### Trivial Changes + +* update interface deps ([#1](https://github.com/libp2p/js-libp2p-peer-id/issues/1)) ([3cf652d](https://github.com/libp2p/js-libp2p-peer-id/commit/3cf652d50ede0d876da46dcb0b1de387126e272a)) + +## [@libp2p/peer-id-v1.1.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.10...@libp2p/peer-id-v1.1.11) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-id-v1.1.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.9...@libp2p/peer-id-v1.1.10) (2022-04-14) + + +### Bug Fixes + +* add logger methods, fix peer id deserialization ([#194](https://github.com/libp2p/js-libp2p-interfaces/issues/194)) ([f0e1fad](https://github.com/libp2p/js-libp2p-interfaces/commit/f0e1fad42701d73eef4233ec2b9a8aafa0b2ab96)) + +## [@libp2p/peer-id-v1.1.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.8...@libp2p/peer-id-v1.1.9) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-id-v1.1.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.7...@libp2p/peer-id-v1.1.8) (2022-03-15) + + +### 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)) + +## [@libp2p/peer-id-v1.1.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.6...@libp2p/peer-id-v1.1.7) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-id-v1.1.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.5...@libp2p/peer-id-v1.1.6) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-id-v1.1.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.4...@libp2p/peer-id-v1.1.5) (2022-02-18) + + +### Bug Fixes + +* simpler pubsub ([#172](https://github.com/libp2p/js-libp2p-interfaces/issues/172)) ([98715ed](https://github.com/libp2p/js-libp2p-interfaces/commit/98715ed73183b32e4fda3d878a462389548358d9)) + +## [@libp2p/peer-id-v1.1.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.3...@libp2p/peer-id-v1.1.4) (2022-02-17) + + +### 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)) + +## [@libp2p/peer-id-v1.1.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.2...@libp2p/peer-id-v1.1.3) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-id-v1.1.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.1...@libp2p/peer-id-v1.1.2) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/peer-id-v1.1.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.0...@libp2p/peer-id-v1.1.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/peer-id-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.4...@libp2p/peer-id-v1.1.0) (2022-02-09) + + +### 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)) + +## [@libp2p/peer-id-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.3...@libp2p/peer-id-v1.0.4) (2022-01-29) + + +### Bug Fixes + +* remove extra fields ([#153](https://github.com/libp2p/js-libp2p-interfaces/issues/153)) ([ccd7cf3](https://github.com/libp2p/js-libp2p-interfaces/commit/ccd7cf3f5ac71337baf516d3b0f6fc724ee0d3b4)) + +## [@libp2p/peer-id-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.2...@libp2p/peer-id-v1.0.3) (2022-01-15) + + +### Trivial Changes + +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/peer-id-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.1...@libp2p/peer-id-v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) +* update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.2.0 (2022-01-04) + + +### Features + +* add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) +* update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) + + + + + +# 0.2.0 (2022-01-02) + + +### Features + +* 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/LICENSE b/packages/peer-id/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-id/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-id/LICENSE-APACHE b/packages/peer-id/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-id/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-id/LICENSE-MIT b/packages/peer-id/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-id/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-id/README.md b/packages/peer-id/README.md new file mode 100644 index 0000000000..7c2c8c23bc --- /dev/null +++ b/packages/peer-id/README.md @@ -0,0 +1,62 @@ +# @libp2p/peer-id + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of @libp2p/interface-peer-id + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +A basic implementation of a peer id + +## Example + +```JavaScript +import { peerIdFromString } from '@libp2p/peer-id' + +const peer = peerIdFromString('k51qzi5uqu5dkwkqm42v9j9kqcam2jiuvloi16g72i4i4amoo2m8u3ol3mqu6s') + +console.log(peer.toCid()) // CID(bafzaa...) +console.log(peer.toString()) // "12D3K..." +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-id/package.json b/packages/peer-id/package.json new file mode 100644 index 0000000000..42a9d54bcd --- /dev/null +++ b/packages/peer-id/package.json @@ -0,0 +1,62 @@ +{ + "name": "@libp2p/peer-id", + "version": "2.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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "multiformats": "^11.0.2", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "aegir": "^39.0.10" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-id/src/index.ts b/packages/peer-id/src/index.ts new file mode 100644 index 0000000000..9ef86986c5 --- /dev/null +++ b/packages/peer-id/src/index.ts @@ -0,0 +1,272 @@ +import { type Ed25519PeerId, type PeerIdType, type RSAPeerId, type Secp256k1PeerId, symbol, type PeerId } from '@libp2p/interface-peer-id' +import { CodeError } from '@libp2p/interfaces/errors' +import { base58btc } from 'multiformats/bases/base58' +import { bases } from 'multiformats/basics' +import { CID } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' +import { identity } from 'multiformats/hashes/identity' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import type { MultibaseDecoder } from 'multiformats/bases/interface' +import type { MultihashDigest } from 'multiformats/hashes/interface' + +const inspect = Symbol.for('nodejs.util.inspect.custom') + +const baseDecoder = Object + .values(bases) + .map(codec => codec.decoder) + // @ts-expect-error https://github.com/multiformats/js-multiformats/issues/141 + .reduce((acc, curr) => acc.or(curr), bases.identity.decoder) + +// these values are from https://github.com/multiformats/multicodec/blob/master/table.csv +const LIBP2P_KEY_CODE = 0x72 + +const MARSHALLED_ED225519_PUBLIC_KEY_LENGTH = 36 +const MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH = 37 + +interface PeerIdInit { + type: PeerIdType + multihash: MultihashDigest + privateKey?: Uint8Array +} + +interface RSAPeerIdInit { + multihash: MultihashDigest + privateKey?: Uint8Array + publicKey?: Uint8Array +} + +interface Ed25519PeerIdInit { + multihash: MultihashDigest + privateKey?: Uint8Array +} + +interface Secp256k1PeerIdInit { + multihash: MultihashDigest + privateKey?: Uint8Array +} + +class PeerIdImpl { + public type: PeerIdType + public readonly multihash: MultihashDigest + public readonly privateKey?: Uint8Array + public readonly publicKey?: Uint8Array + private string?: string + + constructor (init: PeerIdInit) { + this.type = init.type + this.multihash = init.multihash + this.privateKey = init.privateKey + + // mark string cache as non-enumerable + Object.defineProperty(this, 'string', { + enumerable: false, + writable: true + }) + } + + get [Symbol.toStringTag] (): string { + return `PeerId(${this.toString()})` + } + + readonly [symbol] = true + + toString (): string { + if (this.string == null) { + this.string = base58btc.encode(this.multihash.bytes).slice(1) + } + + return this.string + } + + // return self-describing String representation + // in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 + toCID (): CID { + return CID.createV1(LIBP2P_KEY_CODE, this.multihash) + } + + toBytes (): Uint8Array { + return this.multihash.bytes + } + + /** + * Returns Multiaddr as a JSON string + */ + toJSON (): string { + return this.toString() + } + + /** + * Checks the equality of `this` peer against a given PeerId + */ + equals (id: PeerId | Uint8Array | string): boolean { + if (id instanceof Uint8Array) { + return uint8ArrayEquals(this.multihash.bytes, id) + } else if (typeof id === 'string') { + return peerIdFromString(id).equals(this as PeerId) + } else if (id?.multihash?.bytes != null) { + return uint8ArrayEquals(this.multihash.bytes, id.multihash.bytes) + } else { + throw new Error('not valid Id') + } + } + + /** + * Returns PeerId as a human-readable string + * https://nodejs.org/api/util.html#utilinspectcustom + * + * @example + * ```js + * import { peerIdFromString } from '@libp2p/peer-id' + * + * console.info(peerIdFromString('QmFoo')) + * // 'PeerId(QmFoo)' + * ``` + */ + [inspect] (): string { + return `PeerId(${this.toString()})` + } +} + +class RSAPeerIdImpl extends PeerIdImpl implements RSAPeerId { + public readonly type = 'RSA' + public readonly publicKey?: Uint8Array + + constructor (init: RSAPeerIdInit) { + super({ ...init, type: 'RSA' }) + + this.publicKey = init.publicKey + } +} + +class Ed25519PeerIdImpl extends PeerIdImpl implements Ed25519PeerId { + public readonly type = 'Ed25519' + public readonly publicKey: Uint8Array + + constructor (init: Ed25519PeerIdInit) { + super({ ...init, type: 'Ed25519' }) + + this.publicKey = init.multihash.digest + } +} + +class Secp256k1PeerIdImpl extends PeerIdImpl implements Secp256k1PeerId { + public readonly type = 'secp256k1' + public readonly publicKey: Uint8Array + + constructor (init: Secp256k1PeerIdInit) { + super({ ...init, type: 'secp256k1' }) + + this.publicKey = init.multihash.digest + } +} + +export function createPeerId (init: PeerIdInit): PeerId { + if (init.type === 'RSA') { + return new RSAPeerIdImpl(init) + } + + if (init.type === 'Ed25519') { + return new Ed25519PeerIdImpl(init) + } + + if (init.type === 'secp256k1') { + return new Secp256k1PeerIdImpl(init) + } + + throw new CodeError('Type must be "RSA", "Ed25519" or "secp256k1"', 'ERR_INVALID_PARAMETERS') +} + +export function peerIdFromPeerId (other: any): PeerId { + if (other.type === 'RSA') { + return new RSAPeerIdImpl(other) + } + + if (other.type === 'Ed25519') { + return new Ed25519PeerIdImpl(other) + } + + if (other.type === 'secp256k1') { + return new Secp256k1PeerIdImpl(other) + } + + throw new CodeError('Not a PeerId', 'ERR_INVALID_PARAMETERS') +} + +export function peerIdFromString (str: string, decoder?: MultibaseDecoder): PeerId { + decoder = decoder ?? baseDecoder + + if (str.charAt(0) === '1' || str.charAt(0) === 'Q') { + // identity hash ed25519/secp256k1 key or sha2-256 hash of + // rsa public key - base58btc encoded either way + const multihash = Digest.decode(base58btc.decode(`z${str}`)) + + if (str.startsWith('12D')) { + return new Ed25519PeerIdImpl({ multihash }) + } else if (str.startsWith('16U')) { + return new Secp256k1PeerIdImpl({ multihash }) + } else { + return new RSAPeerIdImpl({ multihash }) + } + } + + return peerIdFromBytes(baseDecoder.decode(str)) +} + +export function peerIdFromBytes (buf: Uint8Array): PeerId { + try { + const multihash = Digest.decode(buf) + + if (multihash.code === identity.code) { + if (multihash.digest.length === MARSHALLED_ED225519_PUBLIC_KEY_LENGTH) { + return new Ed25519PeerIdImpl({ multihash }) + } else if (multihash.digest.length === MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH) { + return new Secp256k1PeerIdImpl({ multihash }) + } + } + + if (multihash.code === sha256.code) { + return new RSAPeerIdImpl({ multihash }) + } + } catch { + return peerIdFromCID(CID.decode(buf)) + } + + throw new Error('Supplied PeerID CID is invalid') +} + +export function peerIdFromCID (cid: CID): PeerId { + if (cid == null || cid.multihash == null || cid.version == null || (cid.version === 1 && cid.code !== LIBP2P_KEY_CODE)) { + throw new Error('Supplied PeerID CID is invalid') + } + + const multihash = cid.multihash + + if (multihash.code === sha256.code) { + return new RSAPeerIdImpl({ multihash: cid.multihash }) + } else if (multihash.code === identity.code) { + if (multihash.digest.length === MARSHALLED_ED225519_PUBLIC_KEY_LENGTH) { + return new Ed25519PeerIdImpl({ multihash: cid.multihash }) + } else if (multihash.digest.length === MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH) { + return new Secp256k1PeerIdImpl({ multihash: cid.multihash }) + } + } + + throw new Error('Supplied PeerID CID is invalid') +} + +/** + * @param publicKey - A marshalled public key + * @param privateKey - A marshalled private key + */ +export async function peerIdFromKeys (publicKey: Uint8Array, privateKey?: Uint8Array): Promise { + if (publicKey.length === MARSHALLED_ED225519_PUBLIC_KEY_LENGTH) { + return new Ed25519PeerIdImpl({ multihash: Digest.create(identity.code, publicKey), privateKey }) + } + + if (publicKey.length === MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH) { + return new Secp256k1PeerIdImpl({ multihash: Digest.create(identity.code, publicKey), privateKey }) + } + + return new RSAPeerIdImpl({ multihash: await sha256.digest(publicKey), publicKey, privateKey }) +} diff --git a/packages/peer-id/test/index.spec.ts b/packages/peer-id/test/index.spec.ts new file mode 100644 index 0000000000..98af69f365 --- /dev/null +++ b/packages/peer-id/test/index.spec.ts @@ -0,0 +1,102 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { createPeerId, peerIdFromBytes, peerIdFromString } from '../src/index.js' + +describe('PeerId', () => { + it('create an id without \'new\'', () => { + // @ts-expect-error missing args + expect(() => createPeerId()).to.throw(Error) + }) + + it('create a new id from multihash', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.equals(buf)).to.be.true() + }) + + it('parses a v1 CID with the libp2p-key codec', async () => { + const str = 'bafzaajaiaejca24q7uhr7adt3rtai4ixtn2r3q72kccwvwzg6wnfetwqyvrs5n2d' + const id = peerIdFromString(str) + expect(id.type).to.equal('Ed25519') + expect(id.toString()).to.equal('12D3KooWH4G2B3x5BZHH3j2ccMsBLhzR8u1uzrAQshg429xGFGPk') + expect(id.toCID().toString()).to.equal('bafzaajaiaejca24q7uhr7adt3rtai4ixtn2r3q72kccwvwzg6wnfetwqyvrs5n2d') + }) + + it('defaults to base58btc when stringifying', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.toString()).to.equal('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa') + }) + + it('turns into a CID', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.toCID().toString()).to.equal('bafzaajaiaejcda3tmul6p2537j5upxpjgz3jabbzxqrjqvhhfnthtnezvwibizjh') + }) + + it('equals a Uint8Array', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.equals(buf)).to.be.true() + }) + + it('equals a PeerId', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.equals(peerIdFromBytes(buf))).to.be.true() + }) + + it('parses a PeerId as Ed25519', async () => { + const id = peerIdFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa') + expect(id).to.have.property('type', 'Ed25519') + }) + + it('parses a PeerId as RSA', async () => { + const id = peerIdFromString('QmZHBBrcBtDk7yVzcNUDJBJsZnVGtPHzpTzu16J7Sk6hbp') + expect(id).to.have.property('type', 'RSA') + }) + + it('parses a PeerId as secp256k1', async () => { + const id = peerIdFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL') + expect(id).to.have.property('type', 'secp256k1') + }) + + it('decodes a PeerId as Ed25519', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id).to.have.property('type', 'Ed25519') + }) + + it('decodes a PeerId as RSA', async () => { + const buf = uint8ArrayFromString('QmZHBBrcBtDk7yVzcNUDJBJsZnVGtPHzpTzu16J7Sk6hbp', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id).to.have.property('type', 'RSA') + }) + + it('decodes a PeerId as secp256k1', async () => { + const buf = uint8ArrayFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id).to.have.property('type', 'secp256k1') + }) + + it('caches toString output', async () => { + const buf = uint8ArrayFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL', 'base58btc') + const id = peerIdFromBytes(buf) + + expect(id).to.have.property('string').that.is.not.ok() + + id.toString() + + expect(id).to.have.property('string').that.is.ok() + }) + + it('stringifies as JSON', () => { + const buf = uint8ArrayFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL', 'base58btc') + const id = peerIdFromBytes(buf) + + const res = JSON.parse(JSON.stringify({ id })) + + expect(res).to.have.property('id', id.toString()) + }) +}) diff --git a/packages/peer-id/tsconfig.json b/packages/peer-id/tsconfig.json new file mode 100644 index 0000000000..ba25a6ed4a --- /dev/null +++ b/packages/peer-id/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../interfaces" + } + ] +} diff --git a/packages/peer-record/CHANGELOG.md b/packages/peer-record/CHANGELOG.md new file mode 100644 index 0000000000..fb59f472bf --- /dev/null +++ b/packages/peer-record/CHANGELOG.md @@ -0,0 +1,257 @@ +## [5.0.4](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.3...v5.0.4) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([10f3201](https://github.com/libp2p/js-libp2p-peer-record/commit/10f320136543cf4e41ae6bd2c1da8741ff840e47)) +* Update .github/workflows/stale.yml [skip ci] ([0bd8e9d](https://github.com/libp2p/js-libp2p-peer-record/commit/0bd8e9d32ae78dcba38904ed122bbeb0e1c6c2b6)) + + +### Dependencies + +* **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)) + +## [5.0.3](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.2...v5.0.3) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#55](https://github.com/libp2p/js-libp2p-peer-record/issues/55)) ([edc67ec](https://github.com/libp2p/js-libp2p-peer-record/commit/edc67eceaaf8d1a7f1e41d26b95872860633abf0)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.1...v5.0.2) (2023-03-13) + + +### Bug Fixes + +* remove unused deps ([#52](https://github.com/libp2p/js-libp2p-peer-record/issues/52)) ([9d707bc](https://github.com/libp2p/js-libp2p-peer-record/commit/9d707bc5ea98185c36346d1d58cdd55d723dd7b3)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.0...v5.0.1) (2023-03-10) + + +### Trivial Changes + +* replace err-code with CodeError ([#42](https://github.com/libp2p/js-libp2p-peer-record/issues/42)) ([b76d07f](https://github.com/libp2p/js-libp2p-peer-record/commit/b76d07f07e7de5f65bd00a3c3c4a106fcca99f57)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([157d49d](https://github.com/libp2p/js-libp2p-peer-record/commit/157d49d80d2e6930311aac69980889174addfcdd)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([920dfd3](https://github.com/libp2p/js-libp2p-peer-record/commit/920dfd323e876becae7a98493dc92cd3c40c1e73)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1ef05ef](https://github.com/libp2p/js-libp2p-peer-record/commit/1ef05ef112b5136afe0e3dea00dc3abcf92d2556)) +* upgrade `aegir` to `38.1.2` ([#48](https://github.com/libp2p/js-libp2p-peer-record/issues/48)) ([e0fd5e3](https://github.com/libp2p/js-libp2p-peer-record/commit/e0fd5e3ef3cddd82b4c65817835a783edaebfb3b)) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#45](https://github.com/libp2p/js-libp2p-peer-record/issues/45)) ([83958a5](https://github.com/libp2p/js-libp2p-peer-record/commit/83958a5b1cae2b33cbeee4bb998c790c973db776)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.5...v5.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to 11.x.x (#41) + +### Bug Fixes + +* update multiformats to 11.x.x ([#41](https://github.com/libp2p/js-libp2p-peer-record/issues/41)) ([1c1a4c6](https://github.com/libp2p/js-libp2p-peer-record/commit/1c1a4c6285cc03a7ee61d22a46d35c1b2dee5588)) + + +### Dependencies + +* update logger dep ([c9b6e92](https://github.com/libp2p/js-libp2p-peer-record/commit/c9b6e92d294e2e2f57b573cfaacea1006594c363)) + +## [4.0.5](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.4...v4.0.5) (2022-12-16) + + +### Dependencies + +* bump it-all from 1.0.6 to 2.0.0 ([#32](https://github.com/libp2p/js-libp2p-peer-record/issues/32)) ([fd1f6aa](https://github.com/libp2p/js-libp2p-peer-record/commit/fd1f6aaaeefa3991f9f9dd82342618faae92fbbc)) +* bump it-filter from 1.0.3 to 2.0.0 ([#31](https://github.com/libp2p/js-libp2p-peer-record/issues/31)) ([1369be9](https://github.com/libp2p/js-libp2p-peer-record/commit/1369be940ea2519950aeb0b6fc8969c50b2c0457)), closes [#28](https://github.com/libp2p/js-libp2p-peer-record/issues/28) +* bump it-foreach from 0.1.1 to 1.0.0 ([#33](https://github.com/libp2p/js-libp2p-peer-record/issues/33)) ([64e2fc4](https://github.com/libp2p/js-libp2p-peer-record/commit/64e2fc4a9058b37a542e7571af9b6bf282ac907e)), closes [#28](https://github.com/libp2p/js-libp2p-peer-record/issues/28) +* bump it-map from 1.0.6 to 2.0.0 ([#30](https://github.com/libp2p/js-libp2p-peer-record/issues/30)) ([eb307e4](https://github.com/libp2p/js-libp2p-peer-record/commit/eb307e478c8bf3a3daf1881312d6af6e18dccf14)), closes [#28](https://github.com/libp2p/js-libp2p-peer-record/issues/28) +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#36](https://github.com/libp2p/js-libp2p-peer-record/issues/36)) ([bddbdb7](https://github.com/libp2p/js-libp2p-peer-record/commit/bddbdb7fd367540be8db48d5db32378dbe087e32)) + + +### Documentation + +* publish api docs ([#39](https://github.com/libp2p/js-libp2p-peer-record/issues/39)) ([4477f4b](https://github.com/libp2p/js-libp2p-peer-record/commit/4477f4bdb7fda01fc4a17d85d3e372e81c396674)) + +## [4.0.4](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.3...v4.0.4) (2022-10-12) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#29](https://github.com/libp2p/js-libp2p-peer-record/issues/29)) ([04bf67d](https://github.com/libp2p/js-libp2p-peer-record/commit/04bf67daedbc75a9f7e6144c1bc818cd7f872b5b)) + +## [4.0.3](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.2...v4.0.3) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([11230eb](https://github.com/libp2p/js-libp2p-peer-record/commit/11230eb3a6a6e9791e47b7374e0b606be5ca2cc3)) +* update project config ([#25](https://github.com/libp2p/js-libp2p-peer-record/issues/25)) ([cca2319](https://github.com/libp2p/js-libp2p-peer-record/commit/cca231954701ed2edce6d41d4c3cd3d80d747549)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#26](https://github.com/libp2p/js-libp2p-peer-record/issues/26)) ([9b6f719](https://github.com/libp2p/js-libp2p-peer-record/commit/9b6f719628bcd2a69a23cf672e398b69b4415629)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.1...v4.0.2) (2022-08-12) + + +### Dependencies + +* bump interface-datastore from 6.1.1 to 7.0.0 ([#22](https://github.com/libp2p/js-libp2p-peer-record/issues/22)) ([f516cf0](https://github.com/libp2p/js-libp2p-peer-record/commit/f516cf0e015bbb15810bf3f121c9c911b6ffb160)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.0...v4.0.1) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#21](https://github.com/libp2p/js-libp2p-peer-record/issues/21)) ([9d2e881](https://github.com/libp2p/js-libp2p-peer-record/commit/9d2e88165271c283a6ded4837bc2681fd8cf2f0a)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v3.0.0...v4.0.0) (2022-08-03) + + +### ⚠ BREAKING CHANGES + +* update interface deps to use byte lists (#16) + +### Trivial Changes + +* update project config ([#15](https://github.com/libp2p/js-libp2p-peer-record/issues/15)) ([cd6bded](https://github.com/libp2p/js-libp2p-peer-record/commit/cd6bded2ef90a65c3026bb028879ae6c1f09c260)) + + +### Dependencies + +* update interface deps to use byte lists ([#16](https://github.com/libp2p/js-libp2p-peer-record/issues/16)) ([878f0ca](https://github.com/libp2p/js-libp2p-peer-record/commit/878f0ca0945d535c578c0285b33532bfefd80fbb)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v2.0.2...v3.0.0) (2022-06-27) + + +### ⚠ BREAKING CHANGES + +* **deps:** Bump @libp2p/utils from 2.0.0 to 3.0.0 (#10) + +### Trivial Changes + +* **deps:** Bump @libp2p/utils from 2.0.0 to 3.0.0 ([#10](https://github.com/libp2p/js-libp2p-peer-record/issues/10)) ([17bca9f](https://github.com/libp2p/js-libp2p-peer-record/commit/17bca9fa8447a501ec473f4732017e102ef01141)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-peer-record/compare/v2.0.1...v2.0.2) (2022-06-17) + + +### Trivial Changes + +* update test deps ([#9](https://github.com/libp2p/js-libp2p-peer-record/issues/9)) ([6fb04c4](https://github.com/libp2p/js-libp2p-peer-record/commit/6fb04c45d0f9e7c875b3d7e4228b6d6f78180f92)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-peer-record/compare/v2.0.0...v2.0.1) (2022-06-17) + + +### Trivial Changes + +* **deps:** bump @libp2p/logger from 1.1.6 to 2.0.0 ([#6](https://github.com/libp2p/js-libp2p-peer-record/issues/6)) ([74c21d1](https://github.com/libp2p/js-libp2p-peer-record/commit/74c21d14991ed5f16c372584fc0966ba17bdfc22)) +* **deps:** bump @libp2p/utils from 1.0.10 to 2.0.0 ([#7](https://github.com/libp2p/js-libp2p-peer-record/issues/7)) ([a50d068](https://github.com/libp2p/js-libp2p-peer-record/commit/a50d0685b6c4e2b5038af74f01910deb272efbb4)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v1.0.12...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#3](https://github.com/libp2p/js-libp2p-peer-record/issues/3)) ([3448776](https://github.com/libp2p/js-libp2p-peer-record/commit/34487765885d10fadf2b1e7727e0f1587f72ad97)) + +## [@libp2p/peer-record-v1.0.12](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.11...@libp2p/peer-record-v1.0.12) (2022-05-20) + + +### Bug Fixes + +* update sibling deps ([#216](https://github.com/libp2p/js-libp2p-interfaces/issues/216)) ([0ceca65](https://github.com/libp2p/js-libp2p-interfaces/commit/0ceca658901e92de554c828105b328b88a1416f8)) + +## [@libp2p/peer-record-v1.0.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.10...@libp2p/peer-record-v1.0.11) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-record-v1.0.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.9...@libp2p/peer-record-v1.0.10) (2022-05-10) + + +### Trivial Changes + +* **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) + +## [@libp2p/peer-record-v1.0.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.8...@libp2p/peer-record-v1.0.9) (2022-05-10) + + +### Bug Fixes + +* regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) + +## [@libp2p/peer-record-v1.0.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.7...@libp2p/peer-record-v1.0.8) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-record-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.6...@libp2p/peer-record-v1.0.7) (2022-03-15) + + +### 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)) + +## [@libp2p/peer-record-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.5...@libp2p/peer-record-v1.0.6) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-record-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.4...@libp2p/peer-record-v1.0.5) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-record-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.3...@libp2p/peer-record-v1.0.4) (2022-02-17) + + +### 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)) + +## [@libp2p/peer-record-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.2...@libp2p/peer-record-v1.0.3) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-record-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.1...@libp2p/peer-record-v1.0.2) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/peer-record-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.0...@libp2p/peer-record-v1.0.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## @libp2p/peer-record-v1.0.0 (2022-02-09) + + +### 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)) diff --git a/packages/peer-record/LICENSE b/packages/peer-record/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-record/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-record/LICENSE-APACHE b/packages/peer-record/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-record/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-record/LICENSE-MIT b/packages/peer-record/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-record/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-record/README.md b/packages/peer-record/README.md new file mode 100644 index 0000000000..358efe226b --- /dev/null +++ b/packages/peer-record/README.md @@ -0,0 +1,187 @@ +# @libp2p/peer-record + +[![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) + +> Used to transfer signed peer data across the network + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +Libp2p nodes need to store data in a public location (e.g. a DHT), or rely on potentially untrustworthy intermediaries to relay information over its lifetime. Accordingly, libp2p nodes need to be able to verify that the data came from a specific peer and that it hasn't been tampered with. + +### Envelope + +Libp2p provides an all-purpose data container called **envelope**. It was created to enable the distribution of verifiable records, which we can prove originated from the addressed peer itself. The envelope includes a signature of the data, so that its authenticity is verified. + +This envelope stores a marshaled record implementing the [interface-record](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/record). These Records are designed to be serialized to bytes and placed inside of the envelopes before being shared with other peers. + +You can read further about the envelope in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217). + +## Usage + +- create an envelope with an instance of an [interface-record](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/record) implementation and prepare it for being exchanged: + +```js +// interface-record implementation example with the "libp2p-example" namespace +import { PeerRecord } from '@libp2p/peer-record' +import { fromString } from 'uint8arrays/from-string' + +class ExampleRecord extends PeerRecord { + constructor () { + super ('libp2p-example', fromString('0302', 'hex')) + } + + marshal () {} + + equals (other) {} +} + +ExampleRecord.createFromProtobuf = () => {} +``` + +```js +import { PeerEnvelope } from '@libp2p/peer-record' +import { ExampleRecord } from './example-record.js' + +const rec = new ExampleRecord() +const e = await PeerEnvelope.seal(rec, peerId) +const wireData = e.marshal() +``` + +- consume a received envelope (`wireData`) and transform it back to a record: + +```js +import { PeerEnvelope } from '@libp2p/peer-record' +import { ExampleRecord } from './example-record.js' + +const domain = 'libp2p-example' +let e + +try { + e = await PeerEnvelope.openAndCertify(wireData, domain) +} catch (err) {} + +const rec = ExampleRecord.createFromProtobuf(e.payload) +``` + +## Peer Record + +All libp2p nodes keep a `PeerStore`, that among other information stores a set of known addresses for each peer, which can come from a variety of sources. + +Libp2p peer records were created to enable the distribution of verifiable address records, which we can prove originated from the addressed peer itself. With such guarantees, libp2p is able to prioritize addresses based on their authenticity, with the most strict strategy being to only dial certified addresses (no strategies have been implemented at the time of writing). + +A peer record contains the peers' publicly reachable listen addresses, and may be extended in the future to contain additional metadata relevant to routing. It also contains a `seqNumber` field, a timestamp per the spec, so that we can verify the most recent record. + +You can read further about the Peer Record in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217). + +### Usage + +- create a new Peer Record + +```js +import { PeerRecord } from '@libp2p/peer-record' + +const pr = new PeerRecord({ + peerId: node.peerId, + multiaddrs: node.multiaddrs +}) +``` + +- create a Peer Record from a protobuf + +```js +import { PeerRecord } from '@libp2p/peer-record' + +const pr = PeerRecord.createFromProtobuf(data) +``` + +### Libp2p Flows + +#### Self Record + +Once a libp2p node has started and is listening on a set of multiaddrs, its own peer record can be created. + +The identify service is responsible for creating the self record when the identify protocol kicks in for the first time. This record will be stored for future needs of the identify protocol when connecting with other peers. + +#### Self record Updates + +***NOT\_YET\_IMPLEMENTED*** + +While creating peer records is fairly trivial, addresses are not static and might be modified at arbitrary times. This can happen via an Address Manager API, or even through AutoRelay/AutoNAT. + +When a libp2p node changes its listen addresses, the identify service will be informed. Once that happens, the identify service creates a new self record and stores it. With the new record, the identify push/delta protocol will be used to communicate this change to the connected peers. + +#### Subsystem receiving a record + +Considering that a node can discover other peers' addresses from a variety of sources, Libp2p Peerstore can differentiate the addresses that were obtained through a signed peer record. + +Once a record is received and its signature properly validated, its envelope is stored in the AddressBook in its byte representation. The `seqNumber` remains unmarshalled so that we can quickly compare it against incoming records to determine the most recent record. + +The AddressBook Addresses will be updated with the content of the envelope with a certified property. This allows other subsystems to identify the known certified addresses of a peer. + +#### Subsystem providing a record + +Libp2p subsystems that exchange other peers information will provide the envelope that they received by those peers. As a result, other peers can verify if the envelope was really created by the addressed peer. + +When a subsystem wants to provide a record, it will get it from the AddressBook, if it exists. Other subsystems are also able to provide the self record, since it is also stored in the AddressBook. + +### Future Work + +- Persistence only considering certified addresses? +- Peers may not know their own addresses. It's often impossible to automatically infer one's own public address, and peers may need to rely on third party peers to inform them of their observed public addresses. +- A peer may inadvertently or maliciously sign an address that they do not control. In other words, a signature isn't a guarantee that a given address is valid. +- Some addresses may be ambiguous. For example, addresses on a private subnet are valid within that subnet but are useless on the public internet. +- Once all these pieces are in place, we will also need a way to prioritize addresses based on their authenticity, that is, the dialer can prioritize self-certified addresses over addresses from an unknown origin. + - Modular dialer? (taken from go PR notes) + - With the modular dialer, users should easily be able to configure precedence. With dialer v1, anything we do to prioritise dials is gonna be spaghetti and adhoc. With the modular dialer, you’d be able to specify the order of dials when instantiating the pipeline. + - Multiple parallel dials. We already have the issue where new addresses aren't added to existing dials. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-record/package.json b/packages/peer-record/package.json new file mode 100644 index 0000000000..b3fbd458e2 --- /dev/null +++ b/packages/peer-record/package.json @@ -0,0 +1,80 @@ +{ + "name": "@libp2p/peer-record", + "version": "5.0.4", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/envelope/*.d.ts", + "src/envelope/envelope.js", + "src/peer-record/*.d.ts", + "src/peer-record/peer-record.js" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "generate": "protons src/envelope/envelope.proto src/peer-record/peer-record.proto", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-record": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/multiaddr": "^12.1.3", + "protons-runtime": "^5.0.0", + "uint8-varint": "^1.0.2", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@libp2p/interface-record-compliance-tests": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@types/varint": "^6.0.0", + "aegir": "^39.0.10", + "protons": "^7.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-record/src/envelope/envelope.proto b/packages/peer-record/src/envelope/envelope.proto new file mode 100644 index 0000000000..5b80cf504c --- /dev/null +++ b/packages/peer-record/src/envelope/envelope.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +message Envelope { + // public_key is the public key of the keypair the enclosed payload was + // signed with. + bytes public_key = 1; + + // payload_type encodes the type of payload, so that it can be deserialized + // deterministically. + bytes payload_type = 2; + + // payload is the actual payload carried inside this envelope. + bytes payload = 3; + + // signature is the signature produced by the private key corresponding to + // the enclosed public key, over the payload, prefixing a domain string for + // additional security. + bytes signature = 5; +} \ No newline at end of file diff --git a/packages/peer-record/src/envelope/envelope.ts b/packages/peer-record/src/envelope/envelope.ts new file mode 100644 index 0000000000..5a5d6e640a --- /dev/null +++ b/packages/peer-record/src/envelope/envelope.ts @@ -0,0 +1,97 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Envelope { + publicKey: Uint8Array + payloadType: Uint8Array + payload: Uint8Array + signature: Uint8Array +} + +export namespace Envelope { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.publicKey != null && obj.publicKey.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.publicKey) + } + + if ((obj.payloadType != null && obj.payloadType.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.payloadType) + } + + if ((obj.payload != null && obj.payload.byteLength > 0)) { + w.uint32(26) + w.bytes(obj.payload) + } + + if ((obj.signature != null && obj.signature.byteLength > 0)) { + w.uint32(42) + w.bytes(obj.signature) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + publicKey: new Uint8Array(0), + payloadType: new Uint8Array(0), + payload: new Uint8Array(0), + signature: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.publicKey = reader.bytes() + break + case 2: + obj.payloadType = reader.bytes() + break + case 3: + obj.payload = reader.bytes() + break + case 5: + obj.signature = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Envelope.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Envelope => { + return decodeMessage(buf, Envelope.codec()) + } +} diff --git a/packages/peer-record/src/envelope/index.ts b/packages/peer-record/src/envelope/index.ts new file mode 100644 index 0000000000..6ef0d7951d --- /dev/null +++ b/packages/peer-record/src/envelope/index.ts @@ -0,0 +1,162 @@ +import { unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { CodeError } from '@libp2p/interfaces/errors' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { unsigned } from 'uint8-varint' +import { Uint8ArrayList } from 'uint8arraylist' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8arraysFromString } from 'uint8arrays/from-string' +import { codes } from '../errors.js' +import { Envelope as Protobuf } from './envelope.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Record, Envelope } from '@libp2p/interface-record' + +export interface RecordEnvelopeInit { + peerId: PeerId + payloadType: Uint8Array + payload: Uint8Array + signature: Uint8Array +} + +export class RecordEnvelope implements Envelope { + /** + * Unmarshal a serialized Envelope protobuf message + */ + static createFromProtobuf = async (data: Uint8Array | Uint8ArrayList): Promise => { + const envelopeData = Protobuf.decode(data) + const peerId = await peerIdFromKeys(envelopeData.publicKey) + + return new RecordEnvelope({ + peerId, + payloadType: envelopeData.payloadType, + payload: envelopeData.payload, + signature: envelopeData.signature + }) + } + + /** + * Seal marshals the given Record, places the marshaled bytes inside an Envelope + * and signs it with the given peerId's private key + */ + static seal = async (record: Record, peerId: PeerId): Promise => { + if (peerId.privateKey == null) { + throw new Error('Missing private key') + } + + const domain = record.domain + const payloadType = record.codec + const payload = record.marshal() + const signData = formatSignaturePayload(domain, payloadType, payload) + const key = await unmarshalPrivateKey(peerId.privateKey) + const signature = await key.sign(signData.subarray()) + + return new RecordEnvelope({ + peerId, + payloadType, + payload, + signature + }) + } + + /** + * Open and certify a given marshalled envelope. + * Data is unmarshalled and the signature validated for the given domain. + */ + static openAndCertify = async (data: Uint8Array | Uint8ArrayList, domain: string): Promise => { + const envelope = await RecordEnvelope.createFromProtobuf(data) + const valid = await envelope.validate(domain) + + if (!valid) { + throw new CodeError('envelope signature is not valid for the given domain', codes.ERR_SIGNATURE_NOT_VALID) + } + + return envelope + } + + public peerId: PeerId + public payloadType: Uint8Array + public payload: Uint8Array + public signature: Uint8Array + public marshaled?: Uint8Array + + /** + * The Envelope is responsible for keeping an arbitrary signed record + * by a libp2p peer. + */ + constructor (init: RecordEnvelopeInit) { + const { peerId, payloadType, payload, signature } = init + + this.peerId = peerId + this.payloadType = payloadType + this.payload = payload + this.signature = signature + } + + /** + * Marshal the envelope content + */ + marshal (): Uint8Array { + if (this.peerId.publicKey == null) { + throw new Error('Missing public key') + } + + if (this.marshaled == null) { + this.marshaled = Protobuf.encode({ + publicKey: this.peerId.publicKey, + payloadType: this.payloadType, + payload: this.payload.subarray(), + signature: this.signature + }) + } + + return this.marshaled + } + + /** + * Verifies if the other Envelope is identical to this one + */ + equals (other: Envelope): boolean { + return uint8ArrayEquals(this.marshal(), other.marshal()) + } + + /** + * Validate envelope data signature for the given domain + */ + async validate (domain: string): Promise { + const signData = formatSignaturePayload(domain, this.payloadType, this.payload) + + if (this.peerId.publicKey == null) { + throw new Error('Missing public key') + } + + const key = unmarshalPublicKey(this.peerId.publicKey) + + return key.verify(signData.subarray(), this.signature) + } +} + +/** + * Helper function that prepares a Uint8Array to sign or verify a signature + */ +const formatSignaturePayload = (domain: string, payloadType: Uint8Array, payload: Uint8Array | Uint8ArrayList): Uint8ArrayList => { + // When signing, a peer will prepare a Uint8Array by concatenating the following: + // - The length of the domain separation string string in bytes + // - The domain separation string, encoded as UTF-8 + // - The length of the payload_type field in bytes + // - The value of the payload_type field + // - The length of the payload field in bytes + // - The value of the payload field + + const domainUint8Array = uint8arraysFromString(domain) + const domainLength = unsigned.encode(domainUint8Array.byteLength) + const payloadTypeLength = unsigned.encode(payloadType.length) + const payloadLength = unsigned.encode(payload.length) + + return new Uint8ArrayList( + domainLength, + domainUint8Array, + payloadTypeLength, + payloadType, + payloadLength, + payload + ) +} diff --git a/packages/peer-record/src/errors.ts b/packages/peer-record/src/errors.ts new file mode 100644 index 0000000000..0c09e34d60 --- /dev/null +++ b/packages/peer-record/src/errors.ts @@ -0,0 +1,4 @@ + +export const codes = { + ERR_SIGNATURE_NOT_VALID: 'ERR_SIGNATURE_NOT_VALID' +} diff --git a/packages/peer-record/src/index.ts b/packages/peer-record/src/index.ts new file mode 100644 index 0000000000..1cba7da4de --- /dev/null +++ b/packages/peer-record/src/index.ts @@ -0,0 +1,5 @@ + +export { RecordEnvelope } from './envelope/index.js' +export type { RecordEnvelopeInit } from './envelope/index.js' +export { PeerRecord } from './peer-record/index.js' +export type { PeerRecordInit } from './peer-record/index.js' diff --git a/packages/peer-record/src/peer-record/consts.ts b/packages/peer-record/src/peer-record/consts.ts new file mode 100644 index 0000000000..8f862e33b2 --- /dev/null +++ b/packages/peer-record/src/peer-record/consts.ts @@ -0,0 +1,8 @@ + +// The domain string used for peer records contained in a Envelope. +export const ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record' + +// The type hint used to identify peer records in a Envelope. +// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv +// with name "libp2p-peer-record" +export const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = Uint8Array.from([3, 1]) diff --git a/packages/peer-record/src/peer-record/index.ts b/packages/peer-record/src/peer-record/index.ts new file mode 100644 index 0000000000..bd93db078e --- /dev/null +++ b/packages/peer-record/src/peer-record/index.ts @@ -0,0 +1,104 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { arrayEquals } from '@libp2p/utils/array-equals' +import { multiaddr } from '@multiformats/multiaddr' +import { + ENVELOPE_DOMAIN_PEER_RECORD, + ENVELOPE_PAYLOAD_TYPE_PEER_RECORD +} from './consts.js' +import { PeerRecord as Protobuf } from './peer-record.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface PeerRecordInit { + peerId: PeerId + + /** + * Addresses of the associated peer. + */ + multiaddrs?: Multiaddr[] + + /** + * Monotonically-increasing sequence counter that's used to order PeerRecords in time. + */ + seqNumber?: bigint +} + +/** + * The PeerRecord is used for distributing peer routing records across the network. + * It contains the peer's reachable listen addresses. + */ +export class PeerRecord { + /** + * Unmarshal Peer Record Protobuf + */ + static createFromProtobuf = (buf: Uint8Array | Uint8ArrayList): PeerRecord => { + const peerRecord = Protobuf.decode(buf) + const peerId = peerIdFromBytes(peerRecord.peerId) + const multiaddrs = (peerRecord.addresses ?? []).map((a) => multiaddr(a.multiaddr)) + const seqNumber = peerRecord.seq + + return new PeerRecord({ peerId, multiaddrs, seqNumber }) + } + + static DOMAIN = ENVELOPE_DOMAIN_PEER_RECORD + static CODEC = ENVELOPE_PAYLOAD_TYPE_PEER_RECORD + + public peerId: PeerId + public multiaddrs: Multiaddr[] + public seqNumber: bigint + public domain = PeerRecord.DOMAIN + public codec = PeerRecord.CODEC + private marshaled?: Uint8Array + + constructor (init: PeerRecordInit) { + const { peerId, multiaddrs, seqNumber } = init + + this.peerId = peerId + this.multiaddrs = multiaddrs ?? [] + this.seqNumber = seqNumber ?? BigInt(Date.now()) + } + + /** + * Marshal a record to be used in an envelope + */ + marshal (): Uint8Array { + if (this.marshaled == null) { + this.marshaled = Protobuf.encode({ + peerId: this.peerId.toBytes(), + seq: BigInt(this.seqNumber), + addresses: this.multiaddrs.map((m) => ({ + multiaddr: m.bytes + })) + }) + } + + return this.marshaled + } + + /** + * Returns true if `this` record equals the `other` + */ + equals (other: unknown): boolean { + if (!(other instanceof PeerRecord)) { + return false + } + + // Validate PeerId + if (!this.peerId.equals(other.peerId)) { + return false + } + + // Validate seqNumber + if (this.seqNumber !== other.seqNumber) { + return false + } + + // Validate multiaddrs + if (!arrayEquals(this.multiaddrs, other.multiaddrs)) { + return false + } + + return true + } +} diff --git a/packages/peer-record/src/peer-record/peer-record.proto b/packages/peer-record/src/peer-record/peer-record.proto new file mode 100644 index 0000000000..6b740dc80f --- /dev/null +++ b/packages/peer-record/src/peer-record/peer-record.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +message PeerRecord { + // AddressInfo is a wrapper around a binary multiaddr. It is defined as a + // separate message to allow us to add per-address metadata in the future. + message AddressInfo { + bytes multiaddr = 1; + } + + // peer_id contains a libp2p peer id in its binary representation. + bytes peer_id = 1; + + // seq contains a monotonically-increasing sequence counter to order PeerRecords in time. + uint64 seq = 2; + + // addresses is a list of public listen addresses for the peer. + repeated AddressInfo addresses = 3; +} \ No newline at end of file diff --git a/packages/peer-record/src/peer-record/peer-record.ts b/packages/peer-record/src/peer-record/peer-record.ts new file mode 100644 index 0000000000..0e3e9814fa --- /dev/null +++ b/packages/peer-record/src/peer-record/peer-record.ts @@ -0,0 +1,147 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface PeerRecord { + peerId: Uint8Array + seq: bigint + addresses: PeerRecord.AddressInfo[] +} + +export namespace PeerRecord { + export interface AddressInfo { + multiaddr: Uint8Array + } + + export namespace AddressInfo { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.multiaddr != null && obj.multiaddr.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.multiaddr) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + multiaddr: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.multiaddr = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, AddressInfo.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): AddressInfo => { + return decodeMessage(buf, AddressInfo.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.peerId != null && obj.peerId.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.peerId) + } + + if ((obj.seq != null && obj.seq !== 0n)) { + w.uint32(16) + w.uint64(obj.seq) + } + + if (obj.addresses != null) { + for (const value of obj.addresses) { + w.uint32(26) + PeerRecord.AddressInfo.codec().encode(value, w) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + peerId: new Uint8Array(0), + seq: 0n, + addresses: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.peerId = reader.bytes() + break + case 2: + obj.seq = reader.uint64() + break + case 3: + obj.addresses.push(PeerRecord.AddressInfo.codec().decode(reader, reader.uint32())) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PeerRecord.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PeerRecord => { + return decodeMessage(buf, PeerRecord.codec()) + } +} diff --git a/packages/peer-record/test/envelope.spec.ts b/packages/peer-record/test/envelope.spec.ts new file mode 100644 index 0000000000..1f536288c8 --- /dev/null +++ b/packages/peer-record/test/envelope.spec.ts @@ -0,0 +1,89 @@ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { RecordEnvelope } from '../src/envelope/index.js' +import { codes as ErrorCodes } from '../src/errors.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Record } from '@libp2p/interface-record' + +const domain = 'libp2p-testing' +const codec = uint8arrayFromString('/libp2p/testdata') + +class TestRecord implements Record { + public domain: string + public codec: Uint8Array + public data: string + + constructor (data: string) { + this.domain = domain + this.codec = codec + this.data = data + } + + marshal (): Uint8Array { + return uint8arrayFromString(this.data) + } + + equals (other: Record): boolean { + return uint8ArrayEquals(this.marshal(), other.marshal()) + } +} + +describe('Envelope', () => { + const payloadType = codec + let peerId: PeerId + let testRecord: TestRecord + + before(async () => { + peerId = await createEd25519PeerId() + testRecord = new TestRecord('test-data') + }) + + it('creates an envelope with a random key', () => { + const payload = testRecord.marshal() + const signature = uint8arrayFromString(Math.random().toString(36).substring(7)) + + const envelope = new RecordEnvelope({ + peerId, + payloadType, + payload, + signature + }) + + expect(envelope).to.exist() + expect(envelope.peerId.equals(peerId)).to.eql(true) + expect(envelope.payloadType).to.equalBytes(payloadType) + expect(envelope.payload.subarray()).to.equalBytes(payload.subarray()) + expect(envelope.signature).to.equalBytes(signature) + }) + + it('can seal a record', async () => { + const envelope = await RecordEnvelope.seal(testRecord, peerId) + expect(envelope).to.exist() + expect(envelope.peerId.equals(peerId)).to.eql(true) + expect(envelope.payloadType).to.eql(payloadType) + expect(envelope.payload).to.exist() + expect(envelope.signature).to.exist() + }) + + it('can open and verify a sealed record', async () => { + const envelope = await RecordEnvelope.seal(testRecord, peerId) + const rawEnvelope = envelope.marshal() + + const unmarshalledEnvelope = await RecordEnvelope.openAndCertify(rawEnvelope, testRecord.domain) + expect(unmarshalledEnvelope).to.exist() + + const equals = envelope.equals(unmarshalledEnvelope) + expect(equals).to.eql(true) + }) + + it('throw on open and verify when a different domain is used', async () => { + const envelope = await RecordEnvelope.seal(testRecord, peerId) + const rawEnvelope = envelope.marshal() + + await expect(RecordEnvelope.openAndCertify(rawEnvelope, '/bad-domain')) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_SIGNATURE_NOT_VALID) + }) +}) diff --git a/packages/peer-record/test/peer-record.spec.ts b/packages/peer-record/test/peer-record.spec.ts new file mode 100644 index 0000000000..f2fa396a86 --- /dev/null +++ b/packages/peer-record/test/peer-record.spec.ts @@ -0,0 +1,156 @@ +/* eslint-env mocha */ + +import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import tests from '@libp2p/interface-record-compliance-tests' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { RecordEnvelope } from '../src/envelope/index.js' +import { PeerRecord } from '../src/peer-record/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('interface-record compliance', () => { + tests({ + async setup () { + const peerId = await createEd25519PeerId() + return new PeerRecord({ peerId }) + }, + async teardown () { + // cleanup resources created by setup() + } + }) +}) + +describe('PeerRecord', () => { + let peerId: PeerId + + before(async () => { + peerId = await createEd25519PeerId() + }) + + it('de/serializes the same as a go record', async () => { + const privKey = Uint8Array.from([8, 1, 18, 64, 133, 251, 231, 43, 96, 100, 40, 144, 4, 165, 49, 249, 103, 137, 141, 245, 49, 158, 224, 41, 146, 253, 216, 64, 33, 250, 80, 82, 67, 75, 246, 238, 17, 187, 163, 237, 23, 33, 148, 140, 239, 180, 229, 11, 10, 11, 181, 202, 216, 166, 181, 45, 199, 177, 164, 15, 79, 102, 82, 16, 92, 145, 226, 196]) + const rawEnvelope = Uint8Array.from([10, 36, 8, 1, 18, 32, 17, 187, 163, 237, 23, 33, 148, 140, 239, 180, 229, 11, 10, 11, 181, 202, 216, 166, 181, 45, 199, 177, 164, 15, 79, 102, 82, 16, 92, 145, 226, 196, 18, 2, 3, 1, 26, 170, 1, 10, 38, 0, 36, 8, 1, 18, 32, 17, 187, 163, 237, 23, 33, 148, 140, 239, 180, 229, 11, 10, 11, 181, 202, 216, 166, 181, 45, 199, 177, 164, 15, 79, 102, 82, 16, 92, 145, 226, 196, 16, 216, 184, 224, 191, 147, 145, 182, 151, 22, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 0, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 1, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 2, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 3, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 4, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 5, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 6, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 7, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 8, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 9, 42, 64, 177, 151, 247, 107, 159, 40, 138, 242, 180, 103, 254, 102, 111, 119, 68, 118, 40, 112, 73, 180, 36, 183, 57, 117, 200, 134, 14, 251, 2, 55, 45, 2, 106, 121, 149, 132, 84, 26, 215, 47, 38, 84, 52, 100, 133, 188, 163, 236, 227, 100, 98, 183, 209, 177, 57, 28, 141, 39, 109, 196, 171, 139, 202, 11]) + const key = await unmarshalPrivateKey(privKey) + const peerId = await peerIdFromKeys(key.public.bytes, key.bytes) + + const env = await RecordEnvelope.openAndCertify(rawEnvelope, PeerRecord.DOMAIN) + expect(peerId.equals(env.peerId)) + + const record = PeerRecord.createFromProtobuf(env.payload) + + // The payload isn't going to match because of how the protobuf encodes uint64 values + // They are marshalled correctly on both sides, but will be off by 1 value + // Signatures will still be validated + const jsEnv = await RecordEnvelope.seal(record, peerId) + expect(env.payloadType).to.eql(jsEnv.payloadType) + }) + + it('creates a peer record with peerId', () => { + const peerRecord = new PeerRecord({ peerId }) + + expect(peerRecord).to.exist() + expect(peerRecord.peerId).to.exist() + expect(peerRecord.multiaddrs).to.exist() + expect(peerRecord.multiaddrs).to.have.lengthOf(0) + expect(peerRecord.seqNumber).to.exist() + }) + + it('creates a peer record with provided data', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = BigInt(Date.now()) + const peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + + expect(peerRecord).to.exist() + expect(peerRecord.peerId).to.exist() + expect(peerRecord.multiaddrs).to.exist() + expect(peerRecord.multiaddrs).to.eql(multiaddrs) + expect(peerRecord.seqNumber).to.exist() + expect(peerRecord.seqNumber).to.eql(seqNumber) + }) + + it('marshals and unmarshals a peer record', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = BigInt(Date.now()) + const peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + + // Marshal + const rawData = peerRecord.marshal() + expect(rawData).to.exist() + + // Unmarshal + const unmarshalPeerRecord = PeerRecord.createFromProtobuf(rawData) + expect(unmarshalPeerRecord).to.exist() + + const equals = peerRecord.equals(unmarshalPeerRecord) + expect(equals).to.eql(true) + }) + + it('equals returns false if the peer record has a different peerId', async () => { + const peerRecord0 = new PeerRecord({ peerId }) + + const peerId1 = await createEd25519PeerId() + const peerRecord1 = new PeerRecord({ peerId: peerId1 }) + + const equals = peerRecord0.equals(peerRecord1) + expect(equals).to.eql(false) + }) + + it('equals returns false if the peer record has a different seqNumber', () => { + const ts0 = BigInt(Date.now()) + const peerRecord0 = new PeerRecord({ peerId, seqNumber: ts0 }) + + const ts1 = ts0 + 20n + const peerRecord1 = new PeerRecord({ peerId, seqNumber: ts1 }) + + const equals = peerRecord0.equals(peerRecord1) + expect(equals).to.eql(false) + }) + + it('equals returns false if the peer record has a different multiaddrs', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const peerRecord0 = new PeerRecord({ peerId, multiaddrs }) + + const multiaddrs1 = [ + multiaddr('/ip4/127.0.0.1/tcp/2001') + ] + const peerRecord1 = new PeerRecord({ peerId, multiaddrs: multiaddrs1 }) + + const equals = peerRecord0.equals(peerRecord1) + expect(equals).to.eql(false) + }) +}) + +describe('PeerRecord inside Envelope', () => { + let peerId: PeerId + let peerRecord: PeerRecord + + before(async () => { + peerId = await createEd25519PeerId() + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = BigInt(Date.now()) + peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + }) + + it('creates an envelope with the PeerRecord and can unmarshal it', async () => { + const e = await RecordEnvelope.seal(peerRecord, peerId) + const byteE = e.marshal() + + const decodedE = await RecordEnvelope.openAndCertify(byteE, PeerRecord.DOMAIN) + expect(decodedE).to.exist() + + const decodedPeerRecord = PeerRecord.createFromProtobuf(decodedE.payload) + + const equals = peerRecord.equals(decodedPeerRecord) + expect(equals).to.eql(true) + }) +}) diff --git a/packages/peer-record/tsconfig.json b/packages/peer-record/tsconfig.json new file mode 100644 index 0000000000..27e810f549 --- /dev/null +++ b/packages/peer-record/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-record" + }, + { + "path": "../interface-record-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../utils" + } + ] +} diff --git a/packages/peer-store/CHANGELOG.md b/packages/peer-store/CHANGELOG.md new file mode 100644 index 0000000000..539320f571 --- /dev/null +++ b/packages/peer-store/CHANGELOG.md @@ -0,0 +1,394 @@ +## [8.2.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.2.0...v8.2.1) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([d38eaf8](https://github.com/libp2p/js-libp2p-peer-store/commit/d38eaf85a359761d0d1f9ad127edbe06e6fc88ca)) +* Update .github/workflows/stale.yml [skip ci] ([9fe80a2](https://github.com/libp2p/js-libp2p-peer-store/commit/9fe80a209adbed9c6491fe7be8d80585779cc47f)) + + +### Dependencies + +* **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)) + +## [8.2.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.4...v8.2.0) (2023-06-11) + + +### Features + +* support peer queries ([#88](https://github.com/libp2p/js-libp2p-peer-store/issues/88)) ([6b780fe](https://github.com/libp2p/js-libp2p-peer-store/commit/6b780fe49e81ce82ea83326e76b23390ac966f02)) + +## [8.1.4](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.3...v8.1.4) (2023-06-03) + + +### Tests + +* add tests for patching and merging protocols ([#87](https://github.com/libp2p/js-libp2p-peer-store/issues/87)) ([3e51962](https://github.com/libp2p/js-libp2p-peer-store/commit/3e5196237d3416ca730696bae6ec0797958d90c7)) + +## [8.1.3](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.2...v8.1.3) (2023-06-03) + + +### Dependencies + +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#85](https://github.com/libp2p/js-libp2p-peer-store/issues/85)) ([95fb9a0](https://github.com/libp2p/js-libp2p-peer-store/commit/95fb9a0c2c547483489dc071d34bd0c56dedb330)) +* move @libp2p/peer-id-factory to dependencies ([#86](https://github.com/libp2p/js-libp2p-peer-store/issues/86)) ([7152014](https://github.com/libp2p/js-libp2p-peer-store/commit/71520143e5844a2dc39a03fbcc23938c2d39687d)), closes [/github.com/libp2p/js-libp2p-peer-store/blob/master/src/utils/bytes-to-peer.ts#L2](https://github.com/libp2p//github.com/libp2p/js-libp2p-peer-store/blob/master/src/utils/bytes-to-peer.ts/issues/L2) + +## [8.1.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.1...v8.1.2) (2023-05-10) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#81](https://github.com/libp2p/js-libp2p-peer-store/issues/81)) ([9c8c655](https://github.com/libp2p/js-libp2p-peer-store/commit/9c8c65591c5dafd46afd54646904abc12f50658d)) + +## [8.1.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.0...v8.1.1) (2023-05-10) + + +### Dependencies + +* bump @libp2p/interface-libp2p from 2.0.0 to 3.1.0 ([#83](https://github.com/libp2p/js-libp2p-peer-store/issues/83)) ([9a8d6c6](https://github.com/libp2p/js-libp2p-peer-store/commit/9a8d6c61d3d64dca463c74df2984a4e294424f51)) + +## [8.1.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.0.0...v8.1.0) (2023-05-10) + + +### Features + +* add consume peer record method ([#84](https://github.com/libp2p/js-libp2p-peer-store/issues/84)) ([dcfc803](https://github.com/libp2p/js-libp2p-peer-store/commit/dcfc8030f94d85fe13d93d12188050c5d5cd35a2)) + +## [8.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v7.0.2...v8.0.0) (2023-04-24) + + +### ⚠ BREAKING CHANGES + +* make peerstore atomic (#75) + +### Features + +* make peerstore atomic ([#75](https://github.com/libp2p/js-libp2p-peer-store/issues/75)) ([4e89d3b](https://github.com/libp2p/js-libp2p-peer-store/commit/4e89d3bfeef0b64ccb7ccc09185a9d682ab376e3)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v7.0.1...v7.0.2) (2023-04-11) + + +### Bug Fixes + +* dispatch peer event on adding new addresses as well as set ([#74](https://github.com/libp2p/js-libp2p-peer-store/issues/74)) ([f6d7658](https://github.com/libp2p/js-libp2p-peer-store/commit/f6d76580eb463b09ab737d817d0a0e609212ed6e)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v7.0.0...v7.0.1) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#73](https://github.com/libp2p/js-libp2p-peer-store/issues/73)) ([8ef9aa1](https://github.com/libp2p/js-libp2p-peer-store/commit/8ef9aa1db5baf797e9f1afbaf896801ee0ba3ef4)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.4...v7.0.0) (2023-03-13) + + +### ⚠ BREAKING CHANGES + +* update interface-datastore to 8.x.x (#70) + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([d2b7c22](https://github.com/libp2p/js-libp2p-peer-store/commit/d2b7c229c7497c8a05bed9cb05d85e9da96e9c12)) + + +### Dependencies + +* update interface-datastore to 8.x.x ([#70](https://github.com/libp2p/js-libp2p-peer-store/issues/70)) ([864bd19](https://github.com/libp2p/js-libp2p-peer-store/commit/864bd19e711ed0d0f0a8a509c04b6b3692097232)) + +## [6.0.4](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.3...v6.0.4) (2023-03-02) + + +### Bug Fixes + +* remove it-pipe ([#69](https://github.com/libp2p/js-libp2p-peer-store/issues/69)) ([dcf2e8e](https://github.com/libp2p/js-libp2p-peer-store/commit/dcf2e8e851771ea4dad025c3808d25533a06e651)), closes [#44](https://github.com/libp2p/js-libp2p-peer-store/issues/44) + +## [6.0.3](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.2...v6.0.3) (2023-03-02) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.6 ([#66](https://github.com/libp2p/js-libp2p-peer-store/issues/66)) ([df47658](https://github.com/libp2p/js-libp2p-peer-store/commit/df47658459ef0f691fa509475cea7161ddc0e78e)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.1...v6.0.2) (2023-03-02) + + +### Bug Fixes + +* allow overwriting tags ([#68](https://github.com/libp2p/js-libp2p-peer-store/issues/68)) ([4182211](https://github.com/libp2p/js-libp2p-peer-store/commit/4182211c0bea6df9054bb0050928c68ce467ba2a)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([e1271cf](https://github.com/libp2p/js-libp2p-peer-store/commit/e1271cf951e5e3d9c6f73f0b00c086e63d89f7de)) + + +### Dependencies + +* **dev:** bump protons from 6.1.3 to 7.0.2 ([#60](https://github.com/libp2p/js-libp2p-peer-store/issues/60)) ([0b5e25f](https://github.com/libp2p/js-libp2p-peer-store/commit/0b5e25fdc454958e6d607f046bb2c7253dceb5e4)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.0...v6.0.1) (2023-02-28) + + +### Trivial Changes + +* replace err-code with CodeError ([#53](https://github.com/libp2p/js-libp2p-peer-store/issues/53)) ([e6b87d7](https://github.com/libp2p/js-libp2p-peer-store/commit/e6b87d7f67fdd7124da265a0ca6566989a17a629)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1139dc4](https://github.com/libp2p/js-libp2p-peer-store/commit/1139dc4f495dcd905344c3ed283339ea2b79f5c2)) + + +### Documentation + +* Update API link ([#65](https://github.com/libp2p/js-libp2p-peer-store/issues/65)) ([1b75110](https://github.com/libp2p/js-libp2p-peer-store/commit/1b75110ef6017b82d55134e5fd961b9c5dbfa211)), closes [#64](https://github.com/libp2p/js-libp2p-peer-store/issues/64) + +## [6.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v5.0.1...v6.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 (#52) + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#52](https://github.com/libp2p/js-libp2p-peer-store/issues/52)) ([0b1335c](https://github.com/libp2p/js-libp2p-peer-store/commit/0b1335c1b6f01fc8e750704f0821523fdd2d1502)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v5.0.0...v5.0.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#51](https://github.com/libp2p/js-libp2p-peer-store/issues/51)) ([149e3b9](https://github.com/libp2p/js-libp2p-peer-store/commit/149e3b94746944d0a70f5fd1ff46743e3b4ef13f)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v4.0.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#39](https://github.com/libp2p/js-libp2p-peer-store/issues/39)) ([7434179](https://github.com/libp2p/js-libp2p-peer-store/commit/74341798cc3fb70935fec26004eef2f84a0c269c)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.5...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#36) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#36](https://github.com/libp2p/js-libp2p-peer-store/issues/36)) ([8c76da7](https://github.com/libp2p/js-libp2p-peer-store/commit/8c76da7f581f29cc54a32fbe7b96b87e73370b00)) + +## [3.1.5](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.4...v3.1.5) (2022-09-21) + + +### Bug Fixes + +* do not recreate multiaddr ([#34](https://github.com/libp2p/js-libp2p-peer-store/issues/34)) ([8fbcc57](https://github.com/libp2p/js-libp2p-peer-store/commit/8fbcc57c7f8c027c612f448fcc5a062b3c891971)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#35](https://github.com/libp2p/js-libp2p-peer-store/issues/35)) ([49aa018](https://github.com/libp2p/js-libp2p-peer-store/commit/49aa0189a2e73653bd4cf0335c97721db72e67c0)) + +## [3.1.4](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.3...v3.1.4) (2022-09-20) + + +### Bug Fixes + +* wrong letter in `multiaddrs` of README.md ([#33](https://github.com/libp2p/js-libp2p-peer-store/issues/33)) ([487c059](https://github.com/libp2p/js-libp2p-peer-store/commit/487c059d4798ba6e0dab8db259863aa2cb10d880)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([7a48e4d](https://github.com/libp2p/js-libp2p-peer-store/commit/7a48e4d11e4455e71554ef1c43d1a5650da979e8)) + +## [3.1.3](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.2...v3.1.3) (2022-08-12) + + +### Dependencies + +* update interface-datastore ([#30](https://github.com/libp2p/js-libp2p-peer-store/issues/30)) ([5aff937](https://github.com/libp2p/js-libp2p-peer-store/commit/5aff9373a2757b6ac399e205f077820728d1c72e)) + +## [3.1.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.1...v3.1.2) (2022-08-11) + + +### Dependencies + +* update peer-record ([#27](https://github.com/libp2p/js-libp2p-peer-store/issues/27)) ([e0f6db3](https://github.com/libp2p/js-libp2p-peer-store/commit/e0f6db30ef8e8ebf42d6f5172a652028e2646c5a)) +* update protons to 5.1.0 ([#24](https://github.com/libp2p/js-libp2p-peer-store/issues/24)) ([9eed384](https://github.com/libp2p/js-libp2p-peer-store/commit/9eed3842bace3b3ff15ef5fbbc33ca4288dc79e0)) + +## [3.1.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.0...v3.1.1) (2022-08-03) + + +### Trivial Changes + +* update project ([#20](https://github.com/libp2p/js-libp2p-peer-store/issues/20)) ([fce3db5](https://github.com/libp2p/js-libp2p-peer-store/commit/fce3db536890b4bf223ce0a92453dadb6898440a)) + + +### Dependencies + +* update uint8arraylist and interface deps ([#21](https://github.com/libp2p/js-libp2p-peer-store/issues/21)) ([f693e84](https://github.com/libp2p/js-libp2p-peer-store/commit/f693e8442d342d2e340dcec4ca2c7f5ede91ceb9)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.0.0...v3.1.0) (2022-06-24) + + +### Features + +* add peer tagging ([#12](https://github.com/libp2p/js-libp2p-peer-store/issues/12)) ([c360e41](https://github.com/libp2p/js-libp2p-peer-store/commit/c360e4151e4c5b78d3cbe4658477163e52acc205)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v2.0.0...v3.0.0) (2022-06-16) + + +### ⚠ BREAKING CHANGES + +* registrar API has changed + +### Trivial Changes + +* update deps ([#10](https://github.com/libp2p/js-libp2p-peer-store/issues/10)) ([9d0c7c0](https://github.com/libp2p/js-libp2p-peer-store/commit/9d0c7c05b6b254de7768cf5966729e9e1a53c715)) +* update readme and package.json ([#11](https://github.com/libp2p/js-libp2p-peer-store/issues/11)) ([be2de56](https://github.com/libp2p/js-libp2p-peer-store/commit/be2de561932c7f6af63ec08b82ace75b5f3b9223)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v1.0.17...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +Co-authored-by: Alex Potsides + +### Features + +* update to latest interfaces ([#4](https://github.com/libp2p/js-libp2p-peer-store/issues/4)) ([8cb4a9a](https://github.com/libp2p/js-libp2p-peer-store/commit/8cb4a9a6f4904197552bf26cde62c1ac2f8172d0)) + +### [1.0.17](https://github.com/libp2p/js-libp2p-peer-store/compare/v1.0.16...v1.0.17) (2022-06-09) + + +### Trivial Changes + +* use correct module name in readme ([#1](https://github.com/libp2p/js-libp2p-peer-store/issues/1)) ([4f8377d](https://github.com/libp2p/js-libp2p-peer-store/commit/4f8377dd0f277e6490eabafe8cc4416e03976c14)) + +## [@libp2p/peer-store-v1.0.16](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.15...@libp2p/peer-store-v1.0.16) (2022-05-20) + + +### Bug Fixes + +* update sibling deps ([#216](https://github.com/libp2p/js-libp2p-interfaces/issues/216)) ([0ceca65](https://github.com/libp2p/js-libp2p-interfaces/commit/0ceca658901e92de554c828105b328b88a1416f8)) + +## [@libp2p/peer-store-v1.0.15](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.14...@libp2p/peer-store-v1.0.15) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-store-v1.0.14](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.13...@libp2p/peer-store-v1.0.14) (2022-05-10) + + +### Trivial Changes + +* **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) + +## [@libp2p/peer-store-v1.0.13](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.12...@libp2p/peer-store-v1.0.13) (2022-05-10) + + +### Bug Fixes + +* regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) + +## [@libp2p/peer-store-v1.0.12](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.11...@libp2p/peer-store-v1.0.12) (2022-05-04) + + +### Bug Fixes + +* move startable and events interfaces ([#209](https://github.com/libp2p/js-libp2p-interfaces/issues/209)) ([8ce8a08](https://github.com/libp2p/js-libp2p-interfaces/commit/8ce8a08c94b0738aa32da516558977b195ddd8ed)) + +## [@libp2p/peer-store-v1.0.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.10...@libp2p/peer-store-v1.0.11) (2022-05-01) + + +### Bug Fixes + +* move connection manager mock to connection manager module ([#205](https://github.com/libp2p/js-libp2p-interfaces/issues/205)) ([a367375](https://github.com/libp2p/js-libp2p-interfaces/commit/a367375accc690d7b4608c9a3313f91df700efd8)) + +## [@libp2p/peer-store-v1.0.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.9...@libp2p/peer-store-v1.0.10) (2022-04-19) + + +### Bug Fixes + +* move dev deps to prod ([#195](https://github.com/libp2p/js-libp2p-interfaces/issues/195)) ([3e1ffc7](https://github.com/libp2p/js-libp2p-interfaces/commit/3e1ffc7b174e74be483943ad4e5fcab823ae3f6d)) + +## [@libp2p/peer-store-v1.0.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.8...@libp2p/peer-store-v1.0.9) (2022-04-14) + + +### Bug Fixes + +* add logger methods, fix peer id deserialization ([#194](https://github.com/libp2p/js-libp2p-interfaces/issues/194)) ([f0e1fad](https://github.com/libp2p/js-libp2p-interfaces/commit/f0e1fad42701d73eef4233ec2b9a8aafa0b2ab96)) + +## [@libp2p/peer-store-v1.0.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.7...@libp2p/peer-store-v1.0.8) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-store-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.6...@libp2p/peer-store-v1.0.7) (2022-03-24) + + +### Bug Fixes + +* rename peer data to peer info ([#187](https://github.com/libp2p/js-libp2p-interfaces/issues/187)) ([dfea342](https://github.com/libp2p/js-libp2p-interfaces/commit/dfea3429bad57abde040397e4e7a58539829e9c2)) + +## [@libp2p/peer-store-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.5...@libp2p/peer-store-v1.0.6) (2022-03-15) + + +### 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)) + +## [@libp2p/peer-store-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.4...@libp2p/peer-store-v1.0.5) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-store-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.3...@libp2p/peer-store-v1.0.4) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-store-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.2...@libp2p/peer-store-v1.0.3) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-store-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.1...@libp2p/peer-store-v1.0.2) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/peer-store-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.0...@libp2p/peer-store-v1.0.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## @libp2p/peer-store-v1.0.0 (2022-02-09) + + +### 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)) diff --git a/packages/peer-store/LICENSE b/packages/peer-store/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-store/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-store/LICENSE-APACHE b/packages/peer-store/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-store/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-store/LICENSE-MIT b/packages/peer-store/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-store/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/peer-store/README.md b/packages/peer-store/README.md new file mode 100644 index 0000000000..41a310aad6 --- /dev/null +++ b/packages/peer-store/README.md @@ -0,0 +1,49 @@ +# @libp2p/peer-store + +[![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) + +> Stores information about peers libp2p knows on the network + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[peer-id]: https://github.com/libp2p/js-peer-id + +[peer-store-events]: [https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#libp2ppeerStore] diff --git a/packages/peer-store/package.json b/packages/peer-store/package.json new file mode 100644 index 0000000000..9acd98b945 --- /dev/null +++ b/packages/peer-store/package.json @@ -0,0 +1,87 @@ +{ + "name": "@libp2p/peer-store", + "version": "8.2.1", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/pb/*.d.ts", + "src/pb/peer.js" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "generate": "protons src/pb/*.proto", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-libp2p": "^3.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-collections": "^3.0.0", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@libp2p/peer-record": "^5.0.0", + "@multiformats/multiaddr": "^12.1.3", + "interface-datastore": "^8.2.0", + "it-all": "^3.0.2", + "mortice": "^3.0.1", + "multiformats": "^11.0.2", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@types/sinon": "^10.0.15", + "aegir": "^39.0.10", + "datastore-core": "^9.0.1", + "delay": "^6.0.0", + "p-defer": "^4.0.0", + "p-event": "^6.0.0", + "protons": "^7.0.2", + "sinon": "^15.1.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-store/src/errors.ts b/packages/peer-store/src/errors.ts new file mode 100644 index 0000000000..48c52e7e2c --- /dev/null +++ b/packages/peer-store/src/errors.ts @@ -0,0 +1,4 @@ + +export const codes = { + ERR_INVALID_PARAMETERS: 'ERR_INVALID_PARAMETERS' +} diff --git a/packages/peer-store/src/index.ts b/packages/peer-store/src/index.ts new file mode 100644 index 0000000000..20036c4668 --- /dev/null +++ b/packages/peer-store/src/index.ts @@ -0,0 +1,215 @@ +import { logger } from '@libp2p/logger' +import { RecordEnvelope, PeerRecord } from '@libp2p/peer-record' +import all from 'it-all' +import { PersistentStore, type PeerUpdate } from './store.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore, Peer, PeerData, PeerQuery } from '@libp2p/interface-peer-store' +import type { EventEmitter } from '@libp2p/interfaces/events' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Datastore } from 'interface-datastore' + +const log = logger('libp2p:peer-store') + +export interface PersistentPeerStoreComponents { + peerId: PeerId + datastore: Datastore + events: EventEmitter +} + +/** + * Return true to allow storing the passed multiaddr for the passed peer + */ +export interface AddressFilter { + (peerId: PeerId, multiaddr: Multiaddr): Promise +} + +export interface PersistentPeerStoreInit { + addressFilter?: AddressFilter +} + +/** + * An implementation of PeerStore that stores data in a Datastore + */ +export class PersistentPeerStore implements PeerStore { + private readonly store: PersistentStore + private readonly events: EventEmitter + private readonly peerId: PeerId + + constructor (components: PersistentPeerStoreComponents, init: PersistentPeerStoreInit = {}) { + this.events = components.events + this.peerId = components.peerId + this.store = new PersistentStore(components, init) + } + + async forEach (fn: (peer: Peer,) => void, query?: PeerQuery): Promise { + log.trace('forEach await read lock') + const release = await this.store.lock.readLock() + log.trace('forEach got read lock') + + try { + for await (const peer of this.store.all(query)) { + fn(peer) + } + } finally { + log.trace('forEach release read lock') + release() + } + } + + async all (query?: PeerQuery): Promise { + log.trace('all await read lock') + const release = await this.store.lock.readLock() + log.trace('all got read lock') + + try { + return await all(this.store.all(query)) + } finally { + log.trace('all release read lock') + release() + } + } + + async delete (peerId: PeerId): Promise { + log.trace('delete await write lock') + const release = await this.store.lock.writeLock() + log.trace('delete got write lock') + + try { + await this.store.delete(peerId) + } finally { + log.trace('delete release write lock') + release() + } + } + + async has (peerId: PeerId): Promise { + log.trace('has await read lock') + const release = await this.store.lock.readLock() + log.trace('has got read lock') + + try { + return await this.store.has(peerId) + } finally { + log.trace('has release read lock') + release() + } + } + + async get (peerId: PeerId): Promise { + log.trace('get await read lock') + const release = await this.store.lock.readLock() + log.trace('get got read lock') + + try { + return await this.store.load(peerId) + } finally { + log.trace('get release read lock') + release() + } + } + + async save (id: PeerId, data: PeerData): Promise { + log.trace('save await write lock') + const release = await this.store.lock.writeLock() + log.trace('save got write lock') + + try { + const result = await this.store.save(id, data) + + this.#emitIfUpdated(id, result) + + return result.peer + } finally { + log.trace('save release write lock') + release() + } + } + + async patch (id: PeerId, data: PeerData): Promise { + log.trace('patch await write lock') + const release = await this.store.lock.writeLock() + log.trace('patch got write lock') + + try { + const result = await this.store.patch(id, data) + + this.#emitIfUpdated(id, result) + + return result.peer + } finally { + log.trace('patch release write lock') + release() + } + } + + async merge (id: PeerId, data: PeerData): Promise { + log.trace('merge await write lock') + const release = await this.store.lock.writeLock() + log.trace('merge got write lock') + + try { + const result = await this.store.merge(id, data) + + this.#emitIfUpdated(id, result) + + return result.peer + } finally { + log.trace('merge release write lock') + release() + } + } + + async consumePeerRecord (buf: Uint8Array, expectedPeer?: PeerId): Promise { + const envelope = await RecordEnvelope.openAndCertify(buf, PeerRecord.DOMAIN) + + if (expectedPeer?.equals(envelope.peerId) === false) { + log('envelope peer id was not the expected peer id - expected: %p received: %p', expectedPeer, envelope.peerId) + return false + } + + const peerRecord = PeerRecord.createFromProtobuf(envelope.payload) + let peer: Peer | undefined + + try { + peer = await this.get(envelope.peerId) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + + // ensure seq is greater than, or equal to, the last received + if (peer?.peerRecordEnvelope != null) { + const storedEnvelope = await RecordEnvelope.createFromProtobuf(peer.peerRecordEnvelope) + const storedRecord = PeerRecord.createFromProtobuf(storedEnvelope.payload) + + if (storedRecord.seqNumber >= peerRecord.seqNumber) { + log('sequence number was lower or equal to existing sequence number - stored: %d received: %d', storedRecord.seqNumber, peerRecord.seqNumber) + return false + } + } + + await this.patch(peerRecord.peerId, { + peerRecordEnvelope: buf, + addresses: peerRecord.multiaddrs.map(multiaddr => ({ + isCertified: true, + multiaddr + })) + }) + + return true + } + + #emitIfUpdated (id: PeerId, result: PeerUpdate): void { + if (!result.updated) { + return + } + + if (this.peerId.equals(id)) { + this.events.safeDispatchEvent('self:peer:update', { detail: result }) + } else { + this.events.safeDispatchEvent('peer:update', { detail: result }) + } + } +} diff --git a/packages/peer-store/src/pb/peer.proto b/packages/peer-store/src/pb/peer.proto new file mode 100644 index 0000000000..01c3be1990 --- /dev/null +++ b/packages/peer-store/src/pb/peer.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +message Peer { + // Multiaddrs we know about + repeated Address addresses = 1; + + // The protocols the peer supports + repeated string protocols = 2; + + // The public key of the peer + optional bytes public_key = 4; + + // The most recently received signed PeerRecord + optional bytes peer_record_envelope = 5; + + // Any peer metadata + map metadata = 6; + + // Any tags the peer has + map tags = 7; +} + +// Address represents a single multiaddr +message Address { + bytes multiaddr = 1; + + // Flag to indicate if the address comes from a certified source + optional bool isCertified = 2; +} + +message Tag { + uint32 value = 1; // tag value 0-100 + optional uint64 expiry = 2; // ms timestamp after which the tag is no longer valid +} diff --git a/packages/peer-store/src/pb/peer.ts b/packages/peer-store/src/pb/peer.ts new file mode 100644 index 0000000000..9ceb63f5ee --- /dev/null +++ b/packages/peer-store/src/pb/peer.ts @@ -0,0 +1,396 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Peer { + addresses: Address[] + protocols: string[] + publicKey?: Uint8Array + peerRecordEnvelope?: Uint8Array + metadata: Map + tags: Map +} + +export namespace Peer { + export interface Peer$metadataEntry { + key: string + value: Uint8Array + } + + export namespace Peer$metadataEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key) + } + + if ((obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer$metadataEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer$metadataEntry => { + return decodeMessage(buf, Peer$metadataEntry.codec()) + } + } + + export interface Peer$tagsEntry { + key: string + value?: Tag + } + + export namespace Peer$tagsEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key) + } + + if (obj.value != null) { + w.uint32(18) + Tag.codec().encode(obj.value, w) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = Tag.codec().decode(reader, reader.uint32()) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer$tagsEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer$tagsEntry => { + return decodeMessage(buf, Peer$tagsEntry.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.addresses != null) { + for (const value of obj.addresses) { + w.uint32(10) + Address.codec().encode(value, w) + } + } + + if (obj.protocols != null) { + for (const value of obj.protocols) { + w.uint32(18) + w.string(value) + } + } + + if (obj.publicKey != null) { + w.uint32(34) + w.bytes(obj.publicKey) + } + + if (obj.peerRecordEnvelope != null) { + w.uint32(42) + w.bytes(obj.peerRecordEnvelope) + } + + if (obj.metadata != null && obj.metadata.size !== 0) { + for (const [key, value] of obj.metadata.entries()) { + w.uint32(50) + Peer.Peer$metadataEntry.codec().encode({ key, value }, w) + } + } + + if (obj.tags != null && obj.tags.size !== 0) { + for (const [key, value] of obj.tags.entries()) { + w.uint32(58) + Peer.Peer$tagsEntry.codec().encode({ key, value }, w) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + addresses: [], + protocols: [], + metadata: new Map(), + tags: new Map() + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.addresses.push(Address.codec().decode(reader, reader.uint32())) + break + case 2: + obj.protocols.push(reader.string()) + break + case 4: + obj.publicKey = reader.bytes() + break + case 5: + obj.peerRecordEnvelope = reader.bytes() + break + case 6: { + const entry = Peer.Peer$metadataEntry.codec().decode(reader, reader.uint32()) + obj.metadata.set(entry.key, entry.value) + break + } + case 7: { + const entry = Peer.Peer$tagsEntry.codec().decode(reader, reader.uint32()) + obj.tags.set(entry.key, entry.value) + break + } + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer => { + return decodeMessage(buf, Peer.codec()) + } +} + +export interface Address { + multiaddr: Uint8Array + isCertified?: boolean +} + +export namespace Address { + let _codec: Codec
+ + export const codec = (): Codec
=> { + if (_codec == null) { + _codec = message
((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.multiaddr != null && obj.multiaddr.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.multiaddr) + } + + if (obj.isCertified != null) { + w.uint32(16) + w.bool(obj.isCertified) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + multiaddr: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.multiaddr = reader.bytes() + break + case 2: + obj.isCertified = reader.bool() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial
): Uint8Array => { + return encodeMessage(obj, Address.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Address => { + return decodeMessage(buf, Address.codec()) + } +} + +export interface Tag { + value: number + expiry?: bigint +} + +export namespace Tag { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.value != null && obj.value !== 0)) { + w.uint32(8) + w.uint32(obj.value) + } + + if (obj.expiry != null) { + w.uint32(16) + w.uint64(obj.expiry) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + value: 0 + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.value = reader.uint32() + break + case 2: + obj.expiry = reader.uint64() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Tag.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Tag => { + return decodeMessage(buf, Tag.codec()) + } +} diff --git a/packages/peer-store/src/store.ts b/packages/peer-store/src/store.ts new file mode 100644 index 0000000000..98ef673a6d --- /dev/null +++ b/packages/peer-store/src/store.ts @@ -0,0 +1,187 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { PeerMap } from '@libp2p/peer-collections' +import { peerIdFromBytes } from '@libp2p/peer-id' +import mortice, { type Mortice } from 'mortice' +import { base32 } from 'multiformats/bases/base32' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { codes } from './errors.js' +import { Peer as PeerPB } from './pb/peer.js' +import { bytesToPeer } from './utils/bytes-to-peer.js' +import { NAMESPACE_COMMON, peerIdToDatastoreKey } from './utils/peer-id-to-datastore-key.js' +import { toPeerPB } from './utils/to-peer-pb.js' +import type { AddressFilter, PersistentPeerStoreComponents, PersistentPeerStoreInit } from './index.js' +import type { PeerUpdate as PeerUpdateExternal } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Peer, PeerData, PeerQuery } from '@libp2p/interface-peer-store' +import type { Datastore, Key, Query } from 'interface-datastore' + +/** + * Event detail emitted when peer data changes + */ +export interface PeerUpdate extends PeerUpdateExternal { + updated: boolean +} + +function decodePeer (key: Key, value: Uint8Array, cache: PeerMap): Peer { + // /peers/${peer-id-as-libp2p-key-cid-string-in-base-32} + const base32Str = key.toString().split('/')[2] + const buf = base32.decode(base32Str) + const peerId = peerIdFromBytes(buf) + + const cached = cache.get(peerId) + + if (cached != null) { + return cached + } + + const peer = bytesToPeer(peerId, value) + + cache.set(peerId, peer) + + return peer +} + +function mapQuery (query: PeerQuery, cache: PeerMap): Query { + if (query == null) { + return {} + } + + return { + prefix: NAMESPACE_COMMON, + filters: (query.filters ?? []).map(fn => ({ key, value }) => { + return fn(decodePeer(key, value, cache)) + }), + orders: (query.orders ?? []).map(fn => (a, b) => { + return fn(decodePeer(a.key, a.value, cache), decodePeer(b.key, b.value, cache)) + }) + } +} + +export class PersistentStore { + private readonly peerId: PeerId + private readonly datastore: Datastore + public readonly lock: Mortice + private readonly addressFilter?: AddressFilter + + constructor (components: PersistentPeerStoreComponents, init: PersistentPeerStoreInit = {}) { + this.peerId = components.peerId + this.datastore = components.datastore + this.addressFilter = init.addressFilter + this.lock = mortice({ + name: 'peer-store', + singleProcess: true + }) + } + + async has (peerId: PeerId): Promise { + return this.datastore.has(peerIdToDatastoreKey(peerId)) + } + + async delete (peerId: PeerId): Promise { + if (this.peerId.equals(peerId)) { + throw new CodeError('Cannot delete self peer', codes.ERR_INVALID_PARAMETERS) + } + + await this.datastore.delete(peerIdToDatastoreKey(peerId)) + } + + async load (peerId: PeerId): Promise { + const buf = await this.datastore.get(peerIdToDatastoreKey(peerId)) + + return bytesToPeer(peerId, buf) + } + + async save (peerId: PeerId, data: PeerData): Promise { + const { + existingBuf, + existingPeer + } = await this.#findExistingPeer(peerId) + + const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', { + addressFilter: this.addressFilter + }) + + return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer) + } + + async patch (peerId: PeerId, data: Partial): Promise { + const { + existingBuf, + existingPeer + } = await this.#findExistingPeer(peerId) + + const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', { + addressFilter: this.addressFilter, + existingPeer + }) + + return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer) + } + + async merge (peerId: PeerId, data: PeerData): Promise { + const { + existingBuf, + existingPeer + } = await this.#findExistingPeer(peerId) + + const peerPb: PeerPB = await toPeerPB(peerId, data, 'merge', { + addressFilter: this.addressFilter, + existingPeer + }) + + return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer) + } + + async * all (query?: PeerQuery): AsyncGenerator { + const peerCache = new PeerMap() + + for await (const { key, value } of this.datastore.query(mapQuery(query ?? {}, peerCache))) { + const peer = decodePeer(key, value, peerCache) + + if (peer.id.equals(this.peerId)) { + // Skip self peer if present + continue + } + + yield peer + } + } + + async #findExistingPeer (peerId: PeerId): Promise<{ existingBuf?: Uint8Array, existingPeer?: Peer }> { + try { + const existingBuf = await this.datastore.get(peerIdToDatastoreKey(peerId)) + const existingPeer = bytesToPeer(peerId, existingBuf) + + return { + existingBuf, + existingPeer + } + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + + return {} + } + + async #saveIfDifferent (peerId: PeerId, peer: PeerPB, existingBuf?: Uint8Array, existingPeer?: Peer): Promise { + const buf = PeerPB.encode(peer) + + if (existingBuf != null && uint8ArrayEquals(buf, existingBuf)) { + return { + peer: bytesToPeer(peerId, buf), + previous: existingPeer, + updated: false + } + } + + await this.datastore.put(peerIdToDatastoreKey(peerId), buf) + + return { + peer: bytesToPeer(peerId, buf), + previous: existingPeer, + updated: true + } + } +} diff --git a/packages/peer-store/src/utils/bytes-to-peer.ts b/packages/peer-store/src/utils/bytes-to-peer.ts new file mode 100644 index 0000000000..bd3c7c0d83 --- /dev/null +++ b/packages/peer-store/src/utils/bytes-to-peer.ts @@ -0,0 +1,43 @@ +import { peerIdFromPeerId } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' +import { Peer as PeerPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Peer, Tag } from '@libp2p/interface-peer-store' + +export function bytesToPeer (peerId: PeerId, buf: Uint8Array): Peer { + const peer = PeerPB.decode(buf) + + if (peer.publicKey != null && peerId.publicKey == null) { + peerId = peerIdFromPeerId({ + ...peerId, + publicKey: peerId.publicKey + }) + } + + const tags = new Map() + + // remove any expired tags + const now = BigInt(Date.now()) + + for (const [key, tag] of peer.tags.entries()) { + if (tag.expiry != null && tag.expiry < now) { + continue + } + + tags.set(key, tag) + } + + return { + ...peer, + id: peerId, + addresses: peer.addresses.map(({ multiaddr: ma, isCertified }) => { + return { + multiaddr: multiaddr(ma), + isCertified: isCertified ?? false + } + }), + metadata: peer.metadata, + peerRecordEnvelope: peer.peerRecordEnvelope ?? undefined, + tags + } +} diff --git a/packages/peer-store/src/utils/dedupe-addresses.ts b/packages/peer-store/src/utils/dedupe-addresses.ts new file mode 100644 index 0000000000..ec8e6f6d9d --- /dev/null +++ b/packages/peer-store/src/utils/dedupe-addresses.ts @@ -0,0 +1,51 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { isMultiaddr, multiaddr } from '@multiformats/multiaddr' +import { codes } from '../errors.js' +import type { AddressFilter } from '../index.js' +import type { Address as AddressPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Address } from '@libp2p/interface-peer-store' + +export async function dedupeFilterAndSortAddresses (peerId: PeerId, filter: AddressFilter, addresses: Array
): Promise { + const addressMap = new Map() + + for (const addr of addresses) { + if (addr == null) { + continue + } + + if (addr.multiaddr instanceof Uint8Array) { + addr.multiaddr = multiaddr(addr.multiaddr) + } + + if (!isMultiaddr(addr.multiaddr)) { + throw new CodeError('Multiaddr was invalid', codes.ERR_INVALID_PARAMETERS) + } + + if (!(await filter(peerId, addr.multiaddr))) { + continue + } + + const isCertified = addr.isCertified ?? false + const maStr = addr.multiaddr.toString() + const existingAddr = addressMap.get(maStr) + + if (existingAddr != null) { + addr.isCertified = existingAddr.isCertified || isCertified + } else { + addressMap.set(maStr, { + multiaddr: addr.multiaddr, + isCertified + }) + } + } + + return [...addressMap.values()] + .sort((a, b) => { + return a.multiaddr.toString().localeCompare(b.multiaddr.toString()) + }) + .map(({ isCertified, multiaddr }) => ({ + isCertified, + multiaddr: multiaddr.bytes + })) +} diff --git a/packages/peer-store/src/utils/peer-data-to-datastore-peer.ts b/packages/peer-store/src/utils/peer-data-to-datastore-peer.ts new file mode 100644 index 0000000000..606aa63a5e --- /dev/null +++ b/packages/peer-store/src/utils/peer-data-to-datastore-peer.ts @@ -0,0 +1,116 @@ + +import { CodeError } from '@libp2p/interfaces/errors' +import { isMultiaddr } from '@multiformats/multiaddr' +import { equals as uint8arrayEquals } from 'uint8arrays/equals' +import { codes } from '../errors.js' +import type { Peer as PeerPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +export function toDatastorePeer (peerId: PeerId, data: PeerData): PeerPB { + if (data == null) { + throw new CodeError('Invalid PeerData', codes.ERR_INVALID_PARAMETERS) + } + + if (data.publicKey != null && peerId.publicKey != null && !uint8arrayEquals(data.publicKey, peerId.publicKey)) { + throw new CodeError('publicKey bytes do not match peer id publicKey bytes', codes.ERR_INVALID_PARAMETERS) + } + + // merge addresses and multiaddrs, and dedupe + const addressSet = new Set() + + const output: PeerPB = { + addresses: (data.addresses ?? []) + .concat((data.multiaddrs ?? []).map(multiaddr => ({ multiaddr, isCertified: false }))) + .filter(address => { + if (!isMultiaddr(address.multiaddr)) { + throw new CodeError('Invalid mulitaddr', codes.ERR_INVALID_PARAMETERS) + } + + if (addressSet.has(address.multiaddr.toString())) { + return false + } + + addressSet.add(address.multiaddr.toString()) + return true + }) + .sort((a, b) => { + return a.multiaddr.toString().localeCompare(b.multiaddr.toString()) + }) + .map(({ multiaddr, isCertified }) => ({ + multiaddr: multiaddr.bytes, + isCertified + })), + protocols: (data.protocols ?? []).sort(), + metadata: new Map(), + tags: new Map(), + publicKey: data.publicKey, + peerRecordEnvelope: data.peerRecordEnvelope + } + + // remove invalid metadata + if (data.metadata != null) { + const metadataEntries = data.metadata instanceof Map ? data.metadata.entries() : Object.entries(data.metadata) + + for (const [key, value] of metadataEntries) { + if (typeof key !== 'string') { + throw new CodeError('Peer metadata keys must be strings', codes.ERR_INVALID_PARAMETERS) + } + + if (value == null) { + continue + } + + if (!(value instanceof Uint8Array)) { + throw new CodeError('Peer metadata values must be Uint8Arrays', codes.ERR_INVALID_PARAMETERS) + } + + output.metadata.set(key, value) + } + } + + if (data.tags != null) { + const tagsEntries = data.tags instanceof Map ? data.tags.entries() : Object.entries(data.tags) + + for (const [key, value] of tagsEntries) { + if (typeof key !== 'string') { + throw new CodeError('Peer tag keys must be strings', codes.ERR_INVALID_PARAMETERS) + } + + if (value == null) { + continue + } + + const tag = { + name: key, + ttl: value.ttl, + value: value.value ?? 0 + } + + if (tag.value < 0 || tag.value > 100) { + throw new CodeError('Tag value must be between 0-100', codes.ERR_INVALID_PARAMETERS) + } + + if (parseInt(`${tag.value}`, 10) !== tag.value) { + throw new CodeError('Tag value must be an integer', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.ttl != null) { + if (tag.ttl < 0) { + throw new CodeError('Tag ttl must be between greater than 0', codes.ERR_INVALID_PARAMETERS) + } + + if (parseInt(`${tag.ttl}`, 10) !== tag.ttl) { + throw new CodeError('Tag ttl must be an integer', codes.ERR_INVALID_PARAMETERS) + } + } + + output.tags.set(tag.name, { + value: tag.value, + expiry: tag.ttl == null ? undefined : BigInt(Date.now() + tag.ttl) + }) + } + } + + return output +} diff --git a/packages/peer-store/src/utils/peer-id-to-datastore-key.ts b/packages/peer-store/src/utils/peer-id-to-datastore-key.ts new file mode 100644 index 0000000000..4ff988ac91 --- /dev/null +++ b/packages/peer-store/src/utils/peer-id-to-datastore-key.ts @@ -0,0 +1,15 @@ +import { isPeerId, type PeerId } from '@libp2p/interface-peer-id' +import { CodeError } from '@libp2p/interfaces/errors' +import { Key } from 'interface-datastore/key' +import { codes } from '../errors.js' + +export const NAMESPACE_COMMON = '/peers/' + +export function peerIdToDatastoreKey (peerId: PeerId): Key { + if (!isPeerId(peerId) || peerId.type == null) { + throw new CodeError('Invalid PeerId', codes.ERR_INVALID_PARAMETERS) + } + + const b32key = peerId.toCID().toString() + return new Key(`${NAMESPACE_COMMON}${b32key}`) +} diff --git a/packages/peer-store/src/utils/to-peer-pb.ts b/packages/peer-store/src/utils/to-peer-pb.ts new file mode 100644 index 0000000000..1ba6fa29dd --- /dev/null +++ b/packages/peer-store/src/utils/to-peer-pb.ts @@ -0,0 +1,237 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { equals as uint8arrayEquals } from 'uint8arrays/equals' +import { codes } from '../errors.js' +import { dedupeFilterAndSortAddresses } from './dedupe-addresses.js' +import type { AddressFilter } from '../index.js' +import type { Tag, Peer as PeerPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Address, Peer, PeerData, TagOptions } from '@libp2p/interface-peer-store' + +export interface ToPBPeerOptions { + addressFilter?: AddressFilter + existingPeer?: Peer +} + +export async function toPeerPB (peerId: PeerId, data: Partial, strategy: 'merge' | 'patch', options: ToPBPeerOptions): Promise { + if (data == null) { + throw new CodeError('Invalid PeerData', codes.ERR_INVALID_PARAMETERS) + } + + if (data.publicKey != null && peerId.publicKey != null && !uint8arrayEquals(data.publicKey, peerId.publicKey)) { + throw new CodeError('publicKey bytes do not match peer id publicKey bytes', codes.ERR_INVALID_PARAMETERS) + } + + const existingPeer = options.existingPeer + + if (existingPeer != null && !peerId.equals(existingPeer.id)) { + throw new CodeError('peer id did not match existing peer id', codes.ERR_INVALID_PARAMETERS) + } + + let addresses: Address[] = existingPeer?.addresses ?? [] + let protocols = new Set(existingPeer?.protocols ?? []) + let metadata: Map = existingPeer?.metadata ?? new Map() + let tags: Map = existingPeer?.tags ?? new Map() + let peerRecordEnvelope: Uint8Array | undefined = existingPeer?.peerRecordEnvelope + + // when patching, we replace the original fields with passed values + if (strategy === 'patch') { + if (data.multiaddrs != null || data.addresses != null) { + addresses = [] + + if (data.multiaddrs != null) { + addresses.push(...data.multiaddrs.map(multiaddr => ({ + isCertified: false, + multiaddr + }))) + } + + if (data.addresses != null) { + addresses.push(...data.addresses) + } + } + + if (data.protocols != null) { + protocols = new Set(data.protocols) + } + + if (data.metadata != null) { + const metadataEntries = data.metadata instanceof Map ? [...data.metadata.entries()] : Object.entries(data.metadata) + + metadata = createSortedMap(metadataEntries, { + validate: validateMetadata + }) + } + + if (data.tags != null) { + const tagsEntries = data.tags instanceof Map ? [...data.tags.entries()] : Object.entries(data.tags) + + tags = createSortedMap(tagsEntries, { + validate: validateTag, + map: mapTag + }) + } + + if (data.peerRecordEnvelope != null) { + peerRecordEnvelope = data.peerRecordEnvelope + } + } + + // when merging, we join the original fields with passed values + if (strategy === 'merge') { + if (data.multiaddrs != null) { + addresses.push(...data.multiaddrs.map(multiaddr => ({ + isCertified: false, + multiaddr + }))) + } + + if (data.addresses != null) { + addresses.push(...data.addresses) + } + + if (data.protocols != null) { + protocols = new Set([...protocols, ...data.protocols]) + } + + if (data.metadata != null) { + const metadataEntries = data.metadata instanceof Map ? [...data.metadata.entries()] : Object.entries(data.metadata) + + for (const [key, value] of metadataEntries) { + if (value == null) { + metadata.delete(key) + } else { + metadata.set(key, value) + } + } + + metadata = createSortedMap([...metadata.entries()], { + validate: validateMetadata + }) + } + + if (data.tags != null) { + const tagsEntries = data.tags instanceof Map ? [...data.tags.entries()] : Object.entries(data.tags) + const mergedTags = new Map(tags) + + for (const [key, value] of tagsEntries) { + if (value == null) { + mergedTags.delete(key) + } else { + mergedTags.set(key, value) + } + } + + tags = createSortedMap([...mergedTags.entries()], { + validate: validateTag, + map: mapTag + }) + } + + if (data.peerRecordEnvelope != null) { + peerRecordEnvelope = data.peerRecordEnvelope + } + } + + const output: PeerPB = { + addresses: await dedupeFilterAndSortAddresses(peerId, options.addressFilter ?? (async () => true), addresses), + protocols: [...protocols.values()].sort((a, b) => { + return a.localeCompare(b) + }), + metadata, + tags, + + publicKey: existingPeer?.id.publicKey ?? data.publicKey ?? peerId.publicKey, + peerRecordEnvelope + } + + // Ed25519 and secp256k1 have their public key embedded in them so no need to duplicate it + if (peerId.type !== 'RSA') { + delete output.publicKey + } + + return output +} + +interface CreateSortedMapOptions { + validate: (key: string, value: V) => void + map?: (key: string, value: V) => R +} + +/** + * In JS maps are ordered by insertion order so create a new map with the + * keys inserted in alphabetical order. + */ +function createSortedMap (entries: Array<[string, V | undefined]>, options: CreateSortedMapOptions): Map { + const output = new Map() + + for (const [key, value] of entries) { + if (value == null) { + continue + } + + options.validate(key, value) + } + + for (const [key, value] of entries.sort(([a], [b]) => { + return a.localeCompare(b) + })) { + if (value != null) { + output.set(key, options.map?.(key, value) ?? value) + } + } + + return output +} + +function validateMetadata (key: string, value: Uint8Array): void { + if (typeof key !== 'string') { + throw new CodeError('Metadata key must be a string', codes.ERR_INVALID_PARAMETERS) + } + + if (!(value instanceof Uint8Array)) { + throw new CodeError('Metadata value must be a Uint8Array', codes.ERR_INVALID_PARAMETERS) + } +} + +function validateTag (key: string, tag: TagOptions): void { + if (typeof key !== 'string') { + throw new CodeError('Tag name must be a string', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.value != null) { + if (parseInt(`${tag.value}`, 10) !== tag.value) { + throw new CodeError('Tag value must be an integer', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.value < 0 || tag.value > 100) { + throw new CodeError('Tag value must be between 0-100', codes.ERR_INVALID_PARAMETERS) + } + } + + if (tag.ttl != null) { + if (parseInt(`${tag.ttl}`, 10) !== tag.ttl) { + throw new CodeError('Tag ttl must be an integer', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.ttl < 0) { + throw new CodeError('Tag ttl must be between greater than 0', codes.ERR_INVALID_PARAMETERS) + } + } +} + +function mapTag (key: string, tag: any): Tag { + let expiry: bigint | undefined + + if (tag.expiry != null) { + expiry = tag.expiry + } + + if (tag.ttl != null) { + expiry = BigInt(Date.now() + Number(tag.ttl)) + } + + return { + value: tag.value ?? 0, + expiry + } +} diff --git a/packages/peer-store/test/index.spec.ts b/packages/peer-store/test/index.spec.ts new file mode 100644 index 0000000000..efd9275f58 --- /dev/null +++ b/packages/peer-store/test/index.spec.ts @@ -0,0 +1,287 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { RecordEnvelope, PeerRecord } from '@libp2p/peer-record' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import delay from 'delay' +import { PersistentPeerStore } from '../src/index.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') + +describe('PersistentPeerStore', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('has an empty map of peers', async () => { + const peers = await peerStore.all() + expect(peers.length).to.equal(0) + }) + + describe('has', () => { + it('has peer data', async () => { + await expect(peerStore.has(otherPeerId)).to.eventually.be.false() + await peerStore.save(otherPeerId, { + multiaddrs: [ + addr1 + ] + }) + await expect(peerStore.has(otherPeerId)).to.eventually.be.true() + }) + }) + + describe('delete', () => { + it('deletes peer data', async () => { + await expect(peerStore.has(otherPeerId)).to.eventually.be.false() + await peerStore.save(otherPeerId, { + multiaddrs: [ + addr1 + ] + }) + await expect(peerStore.has(otherPeerId)).to.eventually.be.true() + await peerStore.delete(otherPeerId) + await expect(peerStore.has(otherPeerId)).to.eventually.be.false() + }) + + it('does not allow deleting the self peer', async () => { + await expect(peerStore.has(peerId)).to.eventually.be.false() + await peerStore.save(peerId, { + multiaddrs: [ + addr1 + ] + }) + + await expect(peerStore.delete(peerId)).to.eventually.be.rejected() + .with.property('code', 'ERR_INVALID_PARAMETERS') + }) + }) + + describe('tags', () => { + it('tags a peer', async () => { + const name = 'a-tag' + const peer = await peerStore.save(otherPeerId, { + tags: { + [name]: {} + } + }) + + expect(peer).to.have.property('tags') + .that.deep.equals(new Map([[name, { value: 0 }]]), 'Peer did not contain tag') + }) + + it('tags a peer with a value', async () => { + const name = 'a-tag' + const value = 50 + const peer = await peerStore.save(peerId, { + tags: { + [name]: { value } + } + }) + + expect(peer).to.have.property('tags') + .that.deep.equals(new Map([[name, { value }]]), 'Peer did not contain tag with a value') + }) + + it('tags a peer with a valid value', async () => { + const name = 'a-tag' + + await expect(peerStore.save(peerId, { + tags: { + [name]: { value: -1 } + } + }), 'PeerStore contain tag for peer where value was too small') + .to.eventually.be.rejected().with.property('code', 'ERR_INVALID_PARAMETERS') + + await expect(peerStore.save(peerId, { + tags: { + [name]: { value: 101 } + } + }), 'PeerStore contain tag for peer where value was too large') + .to.eventually.be.rejected().with.property('code', 'ERR_INVALID_PARAMETERS') + + await expect(peerStore.save(peerId, { + tags: { + [name]: { value: 5.5 } + } + }), 'PeerStore contain tag for peer where value was not an integer') + .to.eventually.be.rejected().with.property('code', 'ERR_INVALID_PARAMETERS') + }) + + it('tags a peer with an expiring value', async () => { + const name = 'a-tag' + const value = 50 + const peer = await peerStore.save(peerId, { + tags: { + [name]: { + value, + ttl: 50 + } + } + }) + + expect(peer).to.have.property('tags') + .that.has.key(name) + + await delay(100) + + const updatedPeer = await peerStore.get(peerId) + + expect(updatedPeer).to.have.property('tags') + .that.does.not.have.key(name) + }) + + it('untags a peer', async () => { + const name = 'a-tag' + const peer = await peerStore.save(peerId, { + tags: { + [name]: {} + } + }) + + expect(peer).to.have.property('tags') + .that.has.key(name) + + const updatedPeer = await peerStore.patch(peerId, { + tags: {} + }) + + expect(updatedPeer).to.have.property('tags') + .that.does.not.have.key(name) + }) + }) + + describe('peer record', () => { + it('consumes a peer record, creating a peer', async () => { + const peerRecord = new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/1234') + ] + }) + const signedPeerRecord = await RecordEnvelope.seal(peerRecord, peerId) + + await expect(peerStore.has(peerId)).to.eventually.be.false() + await peerStore.consumePeerRecord(signedPeerRecord.marshal()) + await expect(peerStore.has(peerId)).to.eventually.be.true() + + const peer = await peerStore.get(peerId) + expect(peer.addresses.map(({ multiaddr, isCertified }) => ({ + isCertified, + multiaddr: multiaddr.toString() + }))).to.deep.equal([{ + isCertified: true, + multiaddr: '/ip4/127.0.0.1/tcp/1234' + }]) + }) + + it('overwrites old addresses with those from a peer record', async () => { + await peerStore.patch(peerId, { + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/1234') + ] + }) + + const peerRecord = new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4567') + ] + }) + const signedPeerRecord = await RecordEnvelope.seal(peerRecord, peerId) + + await peerStore.consumePeerRecord(signedPeerRecord.marshal()) + + await expect(peerStore.has(peerId)).to.eventually.be.true() + + const peer = await peerStore.get(peerId) + expect(peer.addresses.map(({ multiaddr, isCertified }) => ({ + isCertified, + multiaddr: multiaddr.toString() + }))).to.deep.equal([{ + isCertified: true, + multiaddr: '/ip4/127.0.0.1/tcp/4567' + }]) + }) + + it('ignores older peer records', async () => { + const oldSignedPeerRecord = await RecordEnvelope.seal(new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/1234') + ], + seqNumber: 1n + }), peerId) + + const newSignedPeerRecord = await RecordEnvelope.seal(new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4567') + ], + seqNumber: 2n + }), peerId) + + await expect(peerStore.consumePeerRecord(newSignedPeerRecord.marshal())).to.eventually.equal(true) + await expect(peerStore.consumePeerRecord(oldSignedPeerRecord.marshal())).to.eventually.equal(false) + + const peer = await peerStore.get(peerId) + expect(peer.addresses.map(({ multiaddr, isCertified }) => ({ + isCertified, + multiaddr: multiaddr.toString() + }))).to.deep.equal([{ + isCertified: true, + multiaddr: '/ip4/127.0.0.1/tcp/4567' + }]) + }) + + it('ignores record for unexpected peer', async () => { + const signedPeerRecord = await RecordEnvelope.seal(new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4567') + ] + }), peerId) + + await expect(peerStore.has(peerId)).to.eventually.be.false() + await expect(peerStore.consumePeerRecord(signedPeerRecord.marshal(), otherPeerId)).to.eventually.equal(false) + await expect(peerStore.has(peerId)).to.eventually.be.false() + }) + + it('allows queries', async () => { + await peerStore.save(otherPeerId, { + multiaddrs: [ + addr1 + ] + }) + + const allPeers = await peerStore.all({ + filters: [ + () => true + ] + }) + + expect(allPeers).to.not.be.empty() + + const noPeers = await peerStore.all({ + filters: [ + () => false + ] + }) + + expect(noPeers).to.be.empty() + }) + }) +}) diff --git a/packages/peer-store/test/merge.spec.ts b/packages/peer-store/test/merge.spec.ts new file mode 100644 index 0000000000..b16a56dc5a --- /dev/null +++ b/packages/peer-store/test/merge.spec.ts @@ -0,0 +1,247 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import { pEvent } from 'p-event' +import { PersistentPeerStore } from '../src/index.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') +const addr3 = multiaddr('/ip4/127.0.0.1/tcp/8002') + +describe('merge', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('emits peer:update event on merge', async () => { + const eventPromise = pEvent(events, 'peer:update') + + await peerStore.merge(otherPeerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('emits self:peer:update event on merge for self peer', async () => { + const eventPromise = pEvent(events, 'self:peer:update') + + await peerStore.merge(peerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('merges multiaddrs', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + multiaddrs: [ + addr3 + ] + }) + + expect(updated).to.have.property('addresses').that.deep.equals([{ + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr3, + isCertified: false + }, { + multiaddr: addr2, + isCertified: false + }]) + + // other fields should be untouched + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges metadata', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]), + baz: Uint8Array.from([6, 7, 8]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + metadata: { + bar: Uint8Array.from([3, 4, 5]), + baz: undefined + } + }) + + expect(updated).to.have.property('metadata').that.deep.equals( + new Map([ + ['foo', Uint8Array.from([0, 1, 2])], + ['bar', Uint8Array.from([3, 4, 5])] + ]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges tags', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 }, + tag3: { value: 50 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.patch(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + tags: { + tag2: { value: 20 }, + tag3: undefined + } + }) + + expect(updated).to.have.property('tags').that.deep.equals( + new Map([ + ['tag1', { value: 10 }], + ['tag2', { value: 20 }] + ]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges protocols', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + protocols: [ + '/bar/foo' + ] + }) + + expect(updated).to.have.property('protocols').that.deep.equals([ + '/bar/foo', + '/foo/bar' + ]) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges peer record envelope', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + peerRecordEnvelope: Uint8Array.from([6, 7, 8]) + }) + + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals( + Uint8Array.from([6, 7, 8]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + }) +}) diff --git a/packages/peer-store/test/patch.spec.ts b/packages/peer-store/test/patch.spec.ts new file mode 100644 index 0000000000..3d64bf4d02 --- /dev/null +++ b/packages/peer-store/test/patch.spec.ts @@ -0,0 +1,231 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import { pEvent } from 'p-event' +import { PersistentPeerStore } from '../src/index.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') +const addr3 = multiaddr('/ip4/127.0.0.1/tcp/8002') + +describe('patch', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('emits peer:update event on patch', async () => { + const eventPromise = pEvent(events, 'peer:update') + + await peerStore.patch(otherPeerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('emits self:peer:update event on patch for self peer', async () => { + const eventPromise = pEvent(events, 'self:peer:update') + + await peerStore.patch(peerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('replaces multiaddrs', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + multiaddrs: [ + addr3 + ] + }) + + // upated field + expect(updated).to.have.property('addresses').that.deep.equals([{ + multiaddr: addr3, + isCertified: false + }]) + + // other fields should be untouched + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces metadata', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + metadata: { + bar: Uint8Array.from([3, 4, 5]) + } + }) + + expect(updated).to.have.property('metadata').that.deep.equals( + new Map([['bar', Uint8Array.from([3, 4, 5])]]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces tags', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + tags: { + tag2: { value: 20 } + } + }) + + expect(updated).to.have.property('tags').that.deep.equals( + new Map([['tag2', { value: 20 }]]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces protocols', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + protocols: [ + '/bar/foo' + ] + }) + + expect(updated).to.have.property('protocols').that.deep.equals([ + '/bar/foo' + ]) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces peer record envelope', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + peerRecordEnvelope: Uint8Array.from([6, 7, 8]) + }) + + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals( + Uint8Array.from([6, 7, 8]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + }) +}) diff --git a/packages/peer-store/test/save.spec.ts b/packages/peer-store/test/save.spec.ts new file mode 100644 index 0000000000..222ee2ea66 --- /dev/null +++ b/packages/peer-store/test/save.spec.ts @@ -0,0 +1,252 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import pDefer from 'p-defer' +import { pEvent } from 'p-event' +import sinon from 'sinon' +import { codes } from '../src/errors.js' +import { PersistentPeerStore } from '../src/index.js' +import { Peer as PeerPB } from '../src/pb/peer.js' +import type { Libp2pEvents, PeerUpdate } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') + +describe('save', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('throws invalid parameters error if invalid PeerId is provided', async () => { + // @ts-expect-error invalid input + await expect(peerStore.save('invalid peerId')) + .to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS) + }) + + it('throws invalid parameters error if no peer data provided', async () => { + // @ts-expect-error invalid input + await expect(peerStore.save(peerId)) + .to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS) + }) + + it('throws invalid parameters error if invalid multiaddrs are provided', async () => { + await expect(peerStore.save(peerId, { + // @ts-expect-error invalid input + addresses: ['invalid multiaddr'] + })) + .to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS) + }) + + it('replaces the stored content by default and emit change event', async () => { + const supportedMultiaddrs = [addr1, addr2] + const eventPromise = pEvent(events, 'peer:update') + + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrs + }) + + const event = await eventPromise as CustomEvent + + const { peer, previous } = event.detail + + expect(peer.addresses).to.deep.equal( + supportedMultiaddrs.map((multiaddr) => ({ + isCertified: false, + multiaddr + })) + ) + expect(previous).to.be.undefined() + }) + + it('emits on set if not storing the exact same content', async () => { + const defer = pDefer() + + const supportedMultiaddrsA = [addr1, addr2] + const supportedMultiaddrsB = [addr2] + + let changeCounter = 0 + events.addEventListener('peer:update', () => { + changeCounter++ + if (changeCounter > 1) { + defer.resolve() + } + }) + + // set 1 + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrsA + }) + + // set 2 + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrsB + }) + + const peer = await peerStore.get(otherPeerId) + const multiaddrs = peer.addresses.map((mi) => mi.multiaddr) + expect(multiaddrs).to.have.deep.members(supportedMultiaddrsB) + + await defer.promise + }) + + it('emits self event on save for self peer', async () => { + const eventPromise = pEvent(events, 'self:peer:update') + + await peerStore.save(peerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('does not emit on set if it is storing the exact same content', async () => { + const defer = pDefer() + + const supportedMultiaddrs = [addr1, addr2] + + let changeCounter = 0 + events.addEventListener('peer:update', () => { + changeCounter++ + if (changeCounter > 1) { + defer.reject(new Error('Saved identical data twice')) + } + }) + + // set 1 + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrs + }) + + // set 2 (same content) + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrs + }) + + // Wait 50ms for incorrect second event + setTimeout(() => { + defer.resolve() + }, 50) + + await defer.promise + }) + + it('should not set public key when key does not match', async () => { + const edKey = await createEd25519PeerId() + + if (peerId.publicKey == null) { + throw new Error('Public key was missing') + } + + await expect(peerStore.save(edKey, { + publicKey: peerId.publicKey + })).to.eventually.be.rejectedWith(/bytes do not match/) + }) + + it('should not store a public key if already stored', async () => { + // @ts-expect-error private fields + const spy = sinon.spy(peerStore.store.datastore, 'put') + + if (otherPeerId.publicKey == null) { + throw new Error('Public key was missing') + } + + // Set PeerId + await peerStore.save(otherPeerId, { + publicKey: otherPeerId.publicKey + }) + await peerStore.save(otherPeerId, { + publicKey: otherPeerId.publicKey + }) + + expect(spy).to.have.property('callCount', 1) + }) + + it('should not store a public key if part of peer id', async () => { + // @ts-expect-error private fields + const spy = sinon.spy(peerStore.store.datastore, 'put') + + if (otherPeerId.publicKey == null) { + throw new Error('Public key was missing') + } + + const edKey = await createEd25519PeerId() + await peerStore.save(edKey, { + publicKey: edKey.publicKey + }) + + const dbPeerEdKey = PeerPB.decode(spy.getCall(0).args[1]) + expect(dbPeerEdKey).to.not.have.property('publicKey') + + const secpKey = await createSecp256k1PeerId() + await peerStore.save(secpKey, { + publicKey: secpKey.publicKey + }) + + const dbPeerSecpKey = PeerPB.decode(spy.getCall(1).args[1]) + expect(dbPeerSecpKey).to.not.have.property('publicKey') + + const rsaKey = await createRSAPeerId() + await peerStore.save(rsaKey, { + publicKey: rsaKey.publicKey + }) + + const dbPeerRsaKey = PeerPB.decode(spy.getCall(2).args[1]) + expect(dbPeerRsaKey).to.have.property('publicKey').that.equalBytes(rsaKey.publicKey) + }) + + it('saves all of the fields', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const saved = await peerStore.save(otherPeerId, peer) + + expect(saved).to.have.property('addresses').that.deep.equals([{ + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr2, + isCertified: false + }]) + expect(saved).to.have.property('metadata').that.deep.equals( + new Map([ + ['foo', Uint8Array.from([0, 1, 2])] + ]) + ) + expect(saved).to.have.property('tags').that.deep.equals( + new Map([ + ['tag1', { value: 10 }] + ]) + ) + expect(saved).to.have.property('protocols').that.deep.equals(peer.protocols) + expect(saved).to.have.property('peerRecordEnvelope').that.deep.equals(peer.peerRecordEnvelope) + }) +}) diff --git a/packages/peer-store/test/utils/dedupe-addresses.spec.ts b/packages/peer-store/test/utils/dedupe-addresses.spec.ts new file mode 100644 index 0000000000..6fa5001007 --- /dev/null +++ b/packages/peer-store/test/utils/dedupe-addresses.spec.ts @@ -0,0 +1,79 @@ +/* eslint-env mocha */ + +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { dedupeFilterAndSortAddresses } from '../../src/utils/dedupe-addresses.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') + +describe('dedupe-addresses', () => { + let peerId: PeerId + + beforeEach(async () => { + peerId = await createEd25519PeerId() + }) + + it('should dedupe addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => true, [{ + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr2, + isCertified: false + }])).to.deep.equal([{ + multiaddr: addr1.bytes, + isCertified: false + }, { + multiaddr: addr2.bytes, + isCertified: false + }]) + }) + + it('should sort addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => true, [{ + multiaddr: addr2, + isCertified: false + }, { + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr1, + isCertified: false + }])).to.deep.equal([{ + multiaddr: addr1.bytes, + isCertified: false + }, { + multiaddr: addr2.bytes, + isCertified: false + }]) + }) + + it('should retain isCertified when deduping addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => true, [{ + multiaddr: addr1, + isCertified: true + }, { + multiaddr: addr1, + isCertified: false + }])).to.deep.equal([{ + multiaddr: addr1.bytes, + isCertified: true + }]) + }) + + it('should filter addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => false, [{ + multiaddr: addr1, + isCertified: true + }, { + multiaddr: addr1, + isCertified: false + }])).to.deep.equal([]) + }) +}) diff --git a/packages/peer-store/tsconfig.json b/packages/peer-store/tsconfig.json new file mode 100644 index 0000000000..7b195f0cb6 --- /dev/null +++ b/packages/peer-store/tsconfig.json @@ -0,0 +1,42 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "exclude": [ + "src/pb/peer.js" + ], + "references": [ + { + "path": "../interface-libp2p" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../peer-record" + } + ] +} diff --git a/packages/record/CHANGELOG.md b/packages/record/CHANGELOG.md new file mode 100644 index 0000000000..4d904e156d --- /dev/null +++ b/packages/record/CHANGELOG.md @@ -0,0 +1,323 @@ +## [3.0.4](https://github.com/libp2p/js-libp2p-record/compare/v3.0.3...v3.0.4) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([afec2c9](https://github.com/libp2p/js-libp2p-record/commit/afec2c9d1685707c9cff82342099839abb6976da)) +* Update .github/workflows/stale.yml [skip ci] ([2d12a78](https://github.com/libp2p/js-libp2p-record/commit/2d12a789581d65181d463fa9b908280b14fc2070)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#95](https://github.com/libp2p/js-libp2p-record/issues/95)) ([30e0fb5](https://github.com/libp2p/js-libp2p-record/commit/30e0fb5dfb193e3289f1aec6c29a8d194fd9aa92)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-record/compare/v3.0.2...v3.0.3) (2023-04-04) + + +### Bug Fixes + +* correction package.json exports types path ([#87](https://github.com/libp2p/js-libp2p-record/issues/87)) ([c1e9a6d](https://github.com/libp2p/js-libp2p-record/commit/c1e9a6d402a971b3ef66484c151b9dc4627fea1b)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-record/compare/v3.0.1...v3.0.2) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#73](https://github.com/libp2p/js-libp2p-record/issues/73)) ([4b1b67b](https://github.com/libp2p/js-libp2p-record/commit/4b1b67bac77cb13a01ce330a4a93eb6c3dc042a5)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-record/compare/v3.0.0...v3.0.1) (2023-03-10) + + +### Trivial Changes + +* replace err-code with CodeError ([#71](https://github.com/libp2p/js-libp2p-record/issues/71)) ([a843ae4](https://github.com/libp2p/js-libp2p-record/commit/a843ae4fbdc8c262a55f4ed87f989770d3783c5a)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([3982918](https://github.com/libp2p/js-libp2p-record/commit/3982918a51c25bf1f803702073eaf17cc5feee9b)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([79984c0](https://github.com/libp2p/js-libp2p-record/commit/79984c0ce651cb0bff634b3b8df630cf807baa59)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7ccccc7](https://github.com/libp2p/js-libp2p-record/commit/7ccccc73fb1aad74cd4b696acd8dc912199afec3)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#84](https://github.com/libp2p/js-libp2p-record/issues/84)) ([4cc5935](https://github.com/libp2p/js-libp2p-record/commit/4cc593576ccda281950f30aa6c8e769baa1aeee6)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-record/compare/v2.0.4...v3.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to 11.x.x (#70) + +### Bug Fixes + +* update multiformats to 11.x.x ([#70](https://github.com/libp2p/js-libp2p-record/issues/70)) ([594fc41](https://github.com/libp2p/js-libp2p-record/commit/594fc4171ec20f4fc1fbc36c99c61eed06aeab25)) + +## [2.0.4](https://github.com/libp2p/js-libp2p-record/compare/v2.0.3...v2.0.4) (2022-12-16) + + +### Documentation + +* publish api docs ([#68](https://github.com/libp2p/js-libp2p-record/issues/68)) ([5a3dd41](https://github.com/libp2p/js-libp2p-record/commit/5a3dd419f13b67e27c19f3b23252937d80fc8b93)) + +## [2.0.3](https://github.com/libp2p/js-libp2p-record/compare/v2.0.2...v2.0.3) (2022-10-12) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([92044b6](https://github.com/libp2p/js-libp2p-record/commit/92044b646180e2b2d0f495d26cc54c184ad6fb7b)) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#63](https://github.com/libp2p/js-libp2p-record/issues/63)) ([9106a6a](https://github.com/libp2p/js-libp2p-record/commit/9106a6abdc71a2c94359759bbc2f61213e9a6a0b)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-record/compare/v2.0.1...v2.0.2) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#58](https://github.com/libp2p/js-libp2p-record/issues/58)) ([24d4047](https://github.com/libp2p/js-libp2p-record/commit/24d404733aa89c28ae71abfcb51ee20b6af919cf)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-record/compare/v2.0.0...v2.0.1) (2022-08-03) + + +### Trivial Changes + +* update project ([#53](https://github.com/libp2p/js-libp2p-record/issues/53)) ([1927144](https://github.com/libp2p/js-libp2p-record/commit/1927144ce346592f513e2f29e0b4677dd1feb468)) + + +### Dependencies + +* update deps to support no-copy operations ([#55](https://github.com/libp2p/js-libp2p-record/issues/55)) ([7be8515](https://github.com/libp2p/js-libp2p-record/commit/7be8515ad87d062bbc9db20fc3134ed06b1286a9)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-record/compare/v1.0.5...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#45](https://github.com/libp2p/js-libp2p-record/issues/45)) ([b5eb989](https://github.com/libp2p/js-libp2p-record/commit/b5eb9897f23ebf39e2a728672f3727222bc1159f)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-record/compare/v1.0.4...v1.0.5) (2022-05-25) + + +### Trivial Changes + +* **deps:** bump @libp2p/interfaces from 1.3.32 to 2.0.2 ([#43](https://github.com/libp2p/js-libp2p-record/issues/43)) ([992677b](https://github.com/libp2p/js-libp2p-record/commit/992677bd6bf432b3ce894c53ac7a721e2dd44bf9)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-record/compare/v1.0.3...v1.0.4) (2022-04-14) + + +### Bug Fixes + +* pad ns correctly ([#41](https://github.com/libp2p/js-libp2p-record/issues/41)) ([18030d9](https://github.com/libp2p/js-libp2p-record/commit/18030d9d3832a7d09dee928923909875a5780a2f)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-record/compare/v1.0.2...v1.0.3) (2022-04-13) + + +### Bug Fixes + +* update interfaces ([#40](https://github.com/libp2p/js-libp2p-record/issues/40)) ([e2713a3](https://github.com/libp2p/js-libp2p-record/commit/e2713a3a6b5351e2dc012cf734ff1c945479920b)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-record/compare/v1.0.1...v1.0.2) (2022-04-09) + + +### Bug Fixes + +* use protons ([#39](https://github.com/libp2p/js-libp2p-record/issues/39)) ([10b4cc2](https://github.com/libp2p/js-libp2p-record/commit/10b4cc2600e8f3bed9a2d646b68b0b2107e1caa4)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-record/compare/v1.0.0...v1.0.1) (2022-03-24) + + +### Bug Fixes + +* export selector/validators with the same name as their prefix ([#34](https://github.com/libp2p/js-libp2p-record/issues/34)) ([4913d1f](https://github.com/libp2p/js-libp2p-record/commit/4913d1fec2ed92d4803f3497bef81142bd560a91)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-record/compare/v0.10.6...v1.0.0) (2022-02-18) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#32](https://github.com/libp2p/js-libp2p-record/issues/32)) ([89cc2ef](https://github.com/libp2p/js-libp2p-record/commit/89cc2ef5234835c82ea29ff54a4887d630921ae3)) + +## [0.10.6](https://github.com/libp2p/js-libp2p-record/compare/v0.10.5...v0.10.6) (2021-09-24) + + +### Bug Fixes + +* auto select if only one record ([#31](https://github.com/libp2p/js-libp2p-record/issues/31)) ([53bc7f2](https://github.com/libp2p/js-libp2p-record/commit/53bc7f2627a95256337033977a05df54a534f951)) + + + +## [0.10.5](https://github.com/libp2p/js-libp2p-record/compare/v0.10.4...v0.10.5) (2021-08-18) + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-record/compare/v0.10.3...v0.10.4) (2021-07-07) + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-record/compare/v0.10.2...v0.10.3) (2021-04-22) + + +### Bug Fixes + +* use dht selectors and validators from interfaces ([#28](https://github.com/libp2p/js-libp2p-record/issues/28)) ([7b211a5](https://github.com/libp2p/js-libp2p-record/commit/7b211a528675018abbc8e4674bedbdd5ab7b5eea)) + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-record/compare/v0.10.1...v0.10.2) (2021-04-20) + + +### Bug Fixes + +* specify pbjs root ([#27](https://github.com/libp2p/js-libp2p-record/issues/27)) ([32ddb1d](https://github.com/libp2p/js-libp2p-record/commit/32ddb1deec71543d0ef34157b6ef2d271e8408f5)) + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-record/compare/v0.10.0...v0.10.1) (2021-04-07) + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-record/compare/v0.8.0...v0.10.0) (2021-02-02) + + +### Features + +* add types and update deps ([#25](https://github.com/libp2p/js-libp2p-record/issues/25)) ([e2395de](https://github.com/libp2p/js-libp2p-record/commit/e2395de924a9c71d761c6ea3f5aab2844b252591)) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-record/compare/v0.8.0...v0.9.0) (2020-08-07) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-record/compare/v0.7.3...v0.8.0) (2020-07-29) + + +### Bug Fixes + +* support uint8arrays in place of node buffers ([#23](https://github.com/libp2p/js-libp2p-record/issues/23)) ([3b99ee1](https://github.com/libp2p/js-libp2p-record/commit/3b99ee1)) + + +### BREAKING CHANGES + +* takes Uint8Arrays as well as Node Buffers + + + + +## [0.7.3](https://github.com/libp2p/js-libp2p-record/compare/v0.7.2...v0.7.3) (2020-04-27) + + +### Bug Fixes + +* remove buffer ([#21](https://github.com/libp2p/js-libp2p-record/issues/21)) ([80fb248](https://github.com/libp2p/js-libp2p-record/commit/80fb248)) + + + + +## [0.7.2](https://github.com/libp2p/js-libp2p-record/compare/v0.7.1...v0.7.2) (2020-02-13) + + +### Bug Fixes + +* remove use of assert module ([#18](https://github.com/libp2p/js-libp2p-record/issues/18)) ([57e24a7](https://github.com/libp2p/js-libp2p-record/commit/57e24a7)) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-record/compare/v0.7.0...v0.7.1) (2020-01-03) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-record/compare/v0.6.3...v0.7.0) (2019-08-16) + + +### Code Refactoring + +* convert from callbacks to async ([#13](https://github.com/libp2p/js-libp2p-record/issues/13)) ([42eab95](https://github.com/libp2p/js-libp2p-record/commit/42eab95)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await + + + + +## [0.6.3](https://github.com/libp2p/js-libp2p-record/compare/v0.6.2...v0.6.3) (2019-05-23) + + +### Bug Fixes + +* remove leftpad ([#16](https://github.com/libp2p/js-libp2p-record/issues/16)) ([4f46885](https://github.com/libp2p/js-libp2p-record/commit/4f46885)) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-record/compare/v0.6.1...v0.6.2) (2019-02-20) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-record/compare/v0.6.0...v0.6.1) (2018-11-08) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-record/compare/v0.5.1...v0.6.0) (2018-10-18) + + +### Features + +* new record definition ([#8](https://github.com/libp2p/js-libp2p-record/issues/8)) ([10177ae](https://github.com/libp2p/js-libp2p-record/commit/10177ae)) + + +### BREAKING CHANGES + +* having the libp2p-record protobuf definition compliant with go-libp2p-record. Author and signature were removed. + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-record/compare/v0.5.0...v0.5.1) (2017-09-07) + + +### Features + +* replace protocol-buffers with protons ([#5](https://github.com/libp2p/js-libp2p-record/issues/5)) ([8774a4f](https://github.com/libp2p/js-libp2p-record/commit/8774a4f)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-record/compare/v0.4.0...v0.5.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#4](https://github.com/libp2p/js-libp2p-record/issues/4)) ([bcba43c](https://github.com/libp2p/js-libp2p-record/commit/bcba43c)) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-record/compare/v0.3.1...v0.4.0) (2017-07-22) + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-record/compare/v0.3.0...v0.3.1) (2017-03-29) + + + + +# 0.3.0 (2017-03-29) diff --git a/packages/record/LICENSE b/packages/record/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/record/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/record/LICENSE-APACHE b/packages/record/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/record/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/record/LICENSE-MIT b/packages/record/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/record/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/record/README.md b/packages/record/README.md new file mode 100644 index 0000000000..1f95184c88 --- /dev/null +++ b/packages/record/README.md @@ -0,0 +1,50 @@ +# @libp2p/record + +[![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) + +> libp2p record implementation + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +Implementation of [go-libp2p-record](https://github.com/libp2p/go-libp2p-record) in JavaScript. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/record/package.json b/packages/record/package.json new file mode 100644 index 0000000000..caa5a086cd --- /dev/null +++ b/packages/record/package.json @@ -0,0 +1,94 @@ +{ + "name": "@libp2p/record", + "version": "3.0.4", + "description": "libp2p record implementation", + "author": "Friedel Ziegelmayer ", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/record#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./selectors": { + "types": "./dist/src/selectors.d.ts", + "import": "./dist/src/selectors.js" + }, + "./validators": { + "types": "./dist/src/validators.d.ts", + "import": "./dist/src/validators.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/record.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "test": "aegir test", + "test:node": "aegir test -t node", + "test:chrome": "aegir test -t browser", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "build": "aegir build", + "generate": "protons ./src/record.proto" + }, + "dependencies": { + "@libp2p/interface-dht": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "multiformats": "^11.0.2", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@libp2p/crypto": "^1.0.0", + "aegir": "^39.0.10", + "protons": "^7.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/record/src/index.ts b/packages/record/src/index.ts new file mode 100644 index 0000000000..06ab8976b9 --- /dev/null +++ b/packages/record/src/index.ts @@ -0,0 +1,70 @@ +import { + Record +} from './record.js' +import * as utils from './utils.js' +import type { Uint8ArrayList } from 'uint8arraylist' + +export class Libp2pRecord { + public key: Uint8Array + public value: Uint8Array + public timeReceived: Date + + constructor (key: Uint8Array, value: Uint8Array, timeReceived: Date) { + if (!(key instanceof Uint8Array)) { + throw new Error('key must be a Uint8Array') + } + + if (!(value instanceof Uint8Array)) { + throw new Error('value must be a Uint8Array') + } + + this.key = key + this.value = value + this.timeReceived = timeReceived + } + + serialize (): Uint8Array { + return Record.encode(this.prepareSerialize()) + } + + /** + * Return the object format ready to be given to the protobuf library. + */ + prepareSerialize (): Record { + return { + key: this.key, + value: this.value, + timeReceived: utils.toRFC3339(this.timeReceived) + } + } + + /** + * Decode a protobuf encoded record + */ + static deserialize (raw: Uint8Array | Uint8ArrayList): Libp2pRecord { + const rec = Record.decode(raw) + + return new Libp2pRecord(rec.key, rec.value, new Date(rec.timeReceived)) + } + + /** + * Create a record from the raw object returned from the protobuf library + */ + static fromDeserialized (obj: Record): Libp2pRecord { + const recvtime = utils.parseRFC3339(obj.timeReceived) + + if (obj.key == null) { + throw new Error('key missing from deserialized object') + } + + if (obj.value == null) { + throw new Error('value missing from deserialized object') + } + + const rec = new Libp2pRecord( + obj.key, obj.value, recvtime + ) + + return rec + } +} diff --git a/packages/record/src/record.proto b/packages/record/src/record.proto new file mode 100644 index 0000000000..ed962bfca0 --- /dev/null +++ b/packages/record/src/record.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +// Record represents a dht record that contains a value +// for a key value pair +message Record { + // The key that references this record + bytes key = 1; + + // The actual value this record is storing + bytes value = 2; + + // Note: These fields were removed from the Record message + // hash of the authors public key + // optional bytes author = 3; + // A PKI signature for the key+value+author + // optional bytes signature = 4; + + // Time the record was received, set by receiver + string timeReceived = 5; +} diff --git a/packages/record/src/record.ts b/packages/record/src/record.ts new file mode 100644 index 0000000000..89da228397 --- /dev/null +++ b/packages/record/src/record.ts @@ -0,0 +1,87 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Record { + key: Uint8Array + value: Uint8Array + timeReceived: string +} + +export namespace Record { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.key) + } + + if ((obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value) + } + + if ((obj.timeReceived != null && obj.timeReceived !== '')) { + w.uint32(42) + w.string(obj.timeReceived) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: new Uint8Array(0), + value: new Uint8Array(0), + timeReceived: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.bytes() + break + case 2: + obj.value = reader.bytes() + break + case 5: + obj.timeReceived = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Record.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Record => { + return decodeMessage(buf, Record.codec()) + } +} diff --git a/packages/record/src/selectors.ts b/packages/record/src/selectors.ts new file mode 100644 index 0000000000..ed28d0124d --- /dev/null +++ b/packages/record/src/selectors.ts @@ -0,0 +1,50 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Selectors } from '@libp2p/interface-dht' + +/** + * Select the best record out of the given records + */ +export function bestRecord (selectors: Selectors, k: Uint8Array, records: Uint8Array[]): number { + if (records.length === 0) { + const errMsg = 'No records given' + + throw new CodeError(errMsg, 'ERR_NO_RECORDS_RECEIVED') + } + + const kStr = uint8ArrayToString(k) + const parts = kStr.split('/') + + if (parts.length < 3) { + const errMsg = 'Record key does not have a selector function' + + throw new CodeError(errMsg, 'ERR_NO_SELECTOR_FUNCTION_FOR_RECORD_KEY') + } + + const selector = selectors[parts[1].toString()] + + if (selector == null) { + const errMsg = `Unrecognized key prefix: ${parts[1]}` + + throw new CodeError(errMsg, 'ERR_UNRECOGNIZED_KEY_PREFIX') + } + + if (records.length === 1) { + return 0 + } + + return selector(k, records) +} + +/** + * Best record selector, for public key records. + * Simply returns the first record, as all valid public key + * records are equal + */ +function publickKey (k: Uint8Array, records: Uint8Array[]): number { + return 0 +} + +export const selectors: Selectors = { + pk: publickKey +} diff --git a/packages/record/src/utils.ts b/packages/record/src/utils.ts new file mode 100644 index 0000000000..b9ee448887 --- /dev/null +++ b/packages/record/src/utils.ts @@ -0,0 +1,46 @@ +/** + * Convert a JavaScript date into an `RFC3339Nano` formatted + * string + */ +export function toRFC3339 (time: Date): string { + const year = time.getUTCFullYear() + const month = String(time.getUTCMonth() + 1).padStart(2, '0') + const day = String(time.getUTCDate()).padStart(2, '0') + const hour = String(time.getUTCHours()).padStart(2, '0') + const minute = String(time.getUTCMinutes()).padStart(2, '0') + const seconds = String(time.getUTCSeconds()).padStart(2, '0') + const milliseconds = time.getUTCMilliseconds() + const nanoseconds = String(milliseconds * 1000 * 1000).padStart(9, '0') + + return `${year}-${month}-${day}T${hour}:${minute}:${seconds}.${nanoseconds}Z` +} + +/** + * Parses a date string formatted as `RFC3339Nano` into a + * JavaScript Date object + */ +export function parseRFC3339 (time: string): Date { + const rfc3339Matcher = new RegExp( + // 2006-01-02T + '(\\d{4})-(\\d{2})-(\\d{2})T' + + // 15:04:05 + '(\\d{2}):(\\d{2}):(\\d{2})' + + // .999999999Z + '\\.(\\d+)Z' + ) + const m = String(time).trim().match(rfc3339Matcher) + + if (m == null) { + throw new Error('Invalid format') + } + + const year = parseInt(m[1], 10) + const month = parseInt(m[2], 10) - 1 + const date = parseInt(m[3], 10) + const hour = parseInt(m[4], 10) + const minute = parseInt(m[5], 10) + const second = parseInt(m[6], 10) + const millisecond = parseInt(m[7].slice(0, -6), 10) + + return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond)) +} diff --git a/packages/record/src/validators.ts b/packages/record/src/validators.ts new file mode 100644 index 0000000000..f0d438b6a7 --- /dev/null +++ b/packages/record/src/validators.ts @@ -0,0 +1,69 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Libp2pRecord } from './index.js' +import type { Validators } from '@libp2p/interface-dht' + +/** + * Checks a record and ensures it is still valid. + * It runs the needed validators. + * If verification fails the returned Promise will reject with the error. + */ +export async function verifyRecord (validators: Validators, record: Libp2pRecord): Promise { + const key = record.key + const keyString = uint8ArrayToString(key) + const parts = keyString.split('/') + + if (parts.length < 3) { + // No validator available + return + } + + const validator = validators[parts[1].toString()] + + if (validator == null) { + const errMsg = 'Invalid record keytype' + + throw new CodeError(errMsg, 'ERR_INVALID_RECORD_KEY_TYPE') + } + + await validator(key, record.value) +} + +/** + * Validator for public key records. + * Verifies that the passed in record value is the PublicKey + * that matches the passed in key. + * If validation fails the returned Promise will reject with the error. + * + * @param {Uint8Array} key - A valid key is of the form `'/pk/'` + * @param {Uint8Array} publicKey - The public key to validate against (protobuf encoded). + */ +const validatePublicKeyRecord = async (key: Uint8Array, publicKey: Uint8Array): Promise => { + if (!(key instanceof Uint8Array)) { + throw new CodeError('"key" must be a Uint8Array', 'ERR_INVALID_RECORD_KEY_NOT_BUFFER') + } + + if (key.byteLength < 5) { + throw new CodeError('invalid public key record', 'ERR_INVALID_RECORD_KEY_TOO_SHORT') + } + + const prefix = uint8ArrayToString(key.subarray(0, 4)) + + if (prefix !== '/pk/') { + throw new CodeError('key was not prefixed with /pk/', 'ERR_INVALID_RECORD_KEY_BAD_PREFIX') + } + + const keyhash = key.slice(4) + + const publicKeyHash = await sha256.digest(publicKey) + + if (!uint8ArrayEquals(keyhash, publicKeyHash.bytes)) { + throw new CodeError('public key does not match passed in key', 'ERR_INVALID_RECORD_HASH_MISMATCH') + } +} + +export const validators: Validators = { + pk: validatePublicKeyRecord +} diff --git a/packages/record/test/fixtures/go-key-records.ts b/packages/record/test/fixtures/go-key-records.ts new file mode 100644 index 0000000000..0bba87ea1d --- /dev/null +++ b/packages/record/test/fixtures/go-key-records.ts @@ -0,0 +1,5 @@ +import { base64pad } from 'multiformats/bases/base64' + +export const publicKey = base64pad.decode( + 'MCAASXjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDjXAQQMal4SB2tSnX6NJIPmC69/BT8A8jc7/gDUZNkEhdhYHvc7k7S4vntV/c92nJGxNdop9fKJyevuNMuXhhHAgMBAAE=' +) diff --git a/packages/record/test/fixtures/go-record.ts b/packages/record/test/fixtures/go-record.ts new file mode 100644 index 0000000000..5582f81ffc --- /dev/null +++ b/packages/record/test/fixtures/go-record.ts @@ -0,0 +1,26 @@ +import { base16 } from 'multiformats/bases/base16' + +// Fixtures generated using gore (https://github.com/motemen/gore) +// +// :import github.com/libp2p/go-libp2p-record +// :import github.com/libp2p/go-libp2p-crypto +// +// priv, pub, err := crypto.GenerateKeyPair(crypto.RSA, 1024) +// +// rec, err := record.MakePutRecord(priv, "hello", []byte("world"), false) +// rec2, err := recordd.MakePutRecord(priv, "hello", []byte("world"), true) +// +// :import github.com/gogo/protobuf/proto +// enc, err := proto.Marshal(rec) +// enc2, err := proto.Marshal(rec2) +// +// :import io/ioutil +// ioutil.WriteFile("js-libp2p-record/test/fixtures/record.bin", enc, 0644) +// ioutil.WriteFile("js-libp2p-record/test/fixtures/record-signed.bin", enc2, 0644) +export const serialized = base16.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116' +) + +export const serializedSigned = base16.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116228001500fe7505698b8a873ccde6f1d36a2be662d57807490d9a9959540f2645a454bf615215092e10123f6ffc4ed694711bfbb1d5ccb62f3da83cf4528ee577a96b6cf0272eef9a920bd56459993690060353b72c22b8c03ad2a33894522dac338905b201179a85cb5e2fc68ed58be96cf89beec6dc0913887dddc10f202a2a1b117' +) diff --git a/packages/record/test/record.spec.ts b/packages/record/test/record.spec.ts new file mode 100644 index 0000000000..ce091b4d4d --- /dev/null +++ b/packages/record/test/record.spec.ts @@ -0,0 +1,49 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Libp2pRecord } from '../src/index.js' +import * as fixture from './fixtures/go-record.js' + +const date = new Date() + +describe('record', () => { + it('new', () => { + const rec = new Libp2pRecord( + uint8ArrayFromString('hello'), + uint8ArrayFromString('world'), + new Date() + ) + + expect(rec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(rec).to.have.property('value').eql(uint8ArrayFromString('world')) + }) + + it('serialize & deserialize', () => { + const rec = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), date) + const dec = Libp2pRecord.deserialize(rec.serialize()) + + expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) + expect(dec.timeReceived).to.be.eql(date) + }) + + it('serialize & deserialize with padding', () => { + // m/d/h/m/s/ms all need padding with 0s when converted to RFC3339 format + const date = new Date('2022-04-03T01:04:08.078Z') + + const rec = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), date) + const dec = Libp2pRecord.deserialize(rec.serialize()) + + expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) + expect(dec.timeReceived).to.be.eql(date) + }) + + describe('go interop', () => { + it('no signature', () => { + const dec = Libp2pRecord.deserialize(fixture.serialized) + expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) + }) + }) +}) diff --git a/packages/record/test/selection.spec.ts b/packages/record/test/selection.spec.ts new file mode 100644 index 0000000000..443be63691 --- /dev/null +++ b/packages/record/test/selection.spec.ts @@ -0,0 +1,73 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as selection from '../src/selectors.js' +import type { Selectors } from '@libp2p/interface-dht' + +const records = [new Uint8Array(), uint8ArrayFromString('hello')] + +describe('selection', () => { + describe('bestRecord', () => { + it('throws no records given when no records received', () => { + expect( + () => selection.bestRecord({}, uint8ArrayFromString('/'), []) + ).to.throw( + /No records given/ + ) + }) + + it('throws on missing selector in the record key', () => { + expect( + () => selection.bestRecord({}, uint8ArrayFromString('/'), records) + ).to.throw( + /Record key does not have a selector function/ + ) + }) + + it('throws on unknown key prefix', () => { + expect( + // @ts-expect-error invalid input + () => selection.bestRecord({ world () {} }, uint8ArrayFromString('/hello/'), records) + ).to.throw( + /Unrecognized key prefix: hello/ + ) + }) + + it('returns the index from the matching selector', () => { + const selectors: Selectors = { + hello (k, recs) { + expect(k).to.be.eql(uint8ArrayFromString('/hello/world')) + expect(recs).to.be.eql(records) + + return 1 + } + } + + expect( + selection.bestRecord(selectors, uint8ArrayFromString('/hello/world'), records) + ).to.equal( + 1 + ) + }) + }) + + describe('selectors', () => { + it('public key', () => { + expect( + selection.selectors.pk(uint8ArrayFromString('/hello/world'), records) + ).to.equal( + 0 + ) + }) + + it('returns the first record when there is only one to select', () => { + expect( + selection.selectors.pk(uint8ArrayFromString('/hello/world'), [records[0]]) + ).to.equal( + 0 + ) + }) + }) +}) diff --git a/packages/record/test/utils.spec.ts b/packages/record/test/utils.spec.ts new file mode 100644 index 0000000000..2babd36d5d --- /dev/null +++ b/packages/record/test/utils.spec.ts @@ -0,0 +1,47 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import * as utils from '../src/utils.js' + +const dates = [{ + obj: new Date(Date.UTC(2016, 0, 1, 8, 22, 33, 392)), + str: '2016-01-01T08:22:33.392000000Z' +}, { + obj: new Date(Date.UTC(2016, 11, 30, 20, 2, 3, 392)), + str: '2016-12-30T20:02:03.392000000Z' +}, { + obj: new Date(Date.UTC(2016, 11, 30, 20, 2, 5, 297)), + str: '2016-12-30T20:02:05.297000000Z' +}, { + obj: new Date(Date.UTC(2012, 1, 25, 10, 10, 10, 10)), + str: '2012-02-25T10:10:10.010000000Z' +}] + +describe('utils', () => { + it('toRFC3339', () => { + dates.forEach((c) => { + expect(utils.toRFC3339(c.obj)).to.be.eql(c.str) + }) + }) + + it('parseRFC3339', () => { + dates.forEach((c) => { + expect(utils.parseRFC3339(c.str)).to.be.eql(c.obj) + }) + }) + + it('to and from RFC3339', () => { + dates.forEach((c) => { + expect( + utils.parseRFC3339(utils.toRFC3339(c.obj)) + ).to.be.eql( + c.obj + ) + expect( + utils.toRFC3339(utils.parseRFC3339(c.str)) + ).to.be.eql( + c.str + ) + }) + }) +}) diff --git a/packages/record/test/validator.spec.ts b/packages/record/test/validator.spec.ts new file mode 100644 index 0000000000..c2c20e187b --- /dev/null +++ b/packages/record/test/validator.spec.ts @@ -0,0 +1,137 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { generateKeyPair, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Libp2pRecord } from '../src/index.js' +import * as validator from '../src/validators.js' +import * as fixture from './fixtures/go-key-records.js' +import type { Validators } from '@libp2p/interface-dht' + +interface Cases { + valid: { + publicKey: Uint8Array[] + } + invalid: { + publicKey: Array<{ + data: Uint8Array + code: string + }> + } +} + +const generateCases = (hash: Uint8Array): Cases => { + return { + valid: { + publicKey: [ + Uint8Array.of( + ...uint8ArrayFromString('/pk/'), + ...hash + ) + ] + }, + invalid: { + publicKey: [{ + data: uint8ArrayFromString('/pk/'), + code: 'ERR_INVALID_RECORD_KEY_TOO_SHORT' + }, { + data: Uint8Array.of(...uint8ArrayFromString('/pk/'), ...uint8ArrayFromString('random')), + code: 'ERR_INVALID_RECORD_HASH_MISMATCH' + }, { + data: hash, + code: 'ERR_INVALID_RECORD_KEY_BAD_PREFIX' + }, { + // @ts-expect-error invalid input + data: 'not a buffer', + code: 'ERR_INVALID_RECORD_KEY_NOT_BUFFER' + }] + } + } +} + +describe('validator', () => { + let key: any + let hash: Uint8Array + let cases: Cases + + before(async () => { + key = await generateKeyPair('RSA', 1024) + hash = await key.public.hash() + cases = generateCases(hash) + }) + + describe('verifyRecord', () => { + it('calls matching validator', async () => { + const k = uint8ArrayFromString('/hello/you') + const rec = new Libp2pRecord(k, uint8ArrayFromString('world'), new Date()) + + const validators: Validators = { + async hello (key, value) { + expect(key).to.eql(k) + expect(value).to.eql(uint8ArrayFromString('world')) + } + } + await validator.verifyRecord(validators, rec) + }) + + it('calls not matching any validator', async () => { + const k = uint8ArrayFromString('/hallo/you') + const rec = new Libp2pRecord(k, uint8ArrayFromString('world'), new Date()) + + const validators: Validators = { + async hello (key, value) { + expect(key).to.eql(k) + expect(value).to.eql(uint8ArrayFromString('world')) + } + } + await expect(validator.verifyRecord(validators, rec)) + .to.eventually.rejectedWith( + /Invalid record keytype/ + ) + }) + }) + + describe('validators', () => { + it('exports pk', () => { + expect(validator.validators).to.have.keys(['pk']) + }) + + describe('public key', () => { + it('exports func', () => { + const pk = validator.validators.pk + + expect(pk).to.be.a('function') + }) + + it('does not error on valid record', async () => { + return Promise.all(cases.valid.publicKey.map(async (k) => { + await validator.validators.pk(k, key.public.bytes) + })) + }) + + it('throws on invalid records', async () => { + return Promise.all(cases.invalid.publicKey.map(async ({ data, code }) => { + try { + // + await validator.validators.pk(data, key.public.bytes) + } catch (err: any) { + expect(err.code).to.eql(code) + return + } + expect.fail('did not throw an error with code ' + code) + })) + }) + }) + }) + + describe('go interop', () => { + it('record with key from from go', async () => { + const pubKey = unmarshalPublicKey(fixture.publicKey) + + const hash = await pubKey.hash() + const k = Uint8Array.of(...uint8ArrayFromString('/pk/'), ...hash) + await validator.validators.pk(k, pubKey.bytes) + }) + }) +}) diff --git a/packages/record/tsconfig.json b/packages/record/tsconfig.json new file mode 100644 index 0000000000..e59bf6ba31 --- /dev/null +++ b/packages/record/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-dht" + }, + { + "path": "../interfaces" + } + ] +} diff --git a/packages/stream-multiplexer-mplex/.aegir.js b/packages/stream-multiplexer-mplex/.aegir.js new file mode 100644 index 0000000000..e6a4b3f7b4 --- /dev/null +++ b/packages/stream-multiplexer-mplex/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + bundlesizeMax: '17KB' + } +} diff --git a/packages/stream-multiplexer-mplex/CHANGELOG.md b/packages/stream-multiplexer-mplex/CHANGELOG.md new file mode 100644 index 0000000000..9018e4926a --- /dev/null +++ b/packages/stream-multiplexer-mplex/CHANGELOG.md @@ -0,0 +1,765 @@ +## [8.0.4](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.3...v8.0.4) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1ebb5e4](https://github.com/libp2p/js-libp2p-mplex/commit/1ebb5e4ba3f3151c3da071e3e1064a2e53791cb2)) +* Update .github/workflows/stale.yml [skip ci] ([74e37d7](https://github.com/libp2p/js-libp2p-mplex/commit/74e37d7ba2c4b83a6b9340d2c9d71d875563f994)) + + +### Dependencies + +* **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)) + +## [8.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.2...v8.0.3) (2023-05-17) + + +### Bug Fixes + +* use abstract stream class from muxer interface ([#279](https://github.com/libp2p/js-libp2p-mplex/issues/279)) ([73df4cf](https://github.com/libp2p/js-libp2p-mplex/commit/73df4cfe933e15ba7c52f1a01649deabf7acf502)) + +## [8.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.1...v8.0.2) (2023-05-17) + + +### Bug Fixes + +* remove unused eslint-plugin-etc dep ([#280](https://github.com/libp2p/js-libp2p-mplex/issues/280)) ([41b9f06](https://github.com/libp2p/js-libp2p-mplex/commit/41b9f06bd352c23fd619ddbf07c72f9c40f40df2)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.6 ([#278](https://github.com/libp2p/js-libp2p-mplex/issues/278)) ([3c7229b](https://github.com/libp2p/js-libp2p-mplex/commit/3c7229bf198cfaf45fc29bc2ac07cb7ff1c83149)) + +## [8.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.0...v8.0.1) (2023-04-19) + + +### Dependencies + +* update abortable-iterator to 5.x.x ([#273](https://github.com/libp2p/js-libp2p-mplex/issues/273)) ([c667204](https://github.com/libp2p/js-libp2p-mplex/commit/c6672049409bf25c07467774bdb46385fbc9a594)) + +## [8.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.7...v8.0.0) (2023-04-18) + + +### ⚠ BREAKING CHANGES + +* update to new stream type deps (#272) + +### Dependencies + +* update to new stream type deps ([#272](https://github.com/libp2p/js-libp2p-mplex/issues/272)) ([04c8c7f](https://github.com/libp2p/js-libp2p-mplex/commit/04c8c7fa69335a3c1495265666140f97a6287626)) + +## [7.1.7](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.6...v7.1.7) (2023-04-13) + + +### Dependencies + +* **dev:** bump it-drain from 2.0.1 to 3.0.1 ([#262](https://github.com/libp2p/js-libp2p-mplex/issues/262)) ([d96125b](https://github.com/libp2p/js-libp2p-mplex/commit/d96125be24ac80598f56de49c918dcba8b4db2b5)) +* **dev:** bump it-to-buffer from 3.0.1 to 4.0.1 ([#258](https://github.com/libp2p/js-libp2p-mplex/issues/258)) ([59e7558](https://github.com/libp2p/js-libp2p-mplex/commit/59e755892287c86bfd706e9dbfa942ce410067d1)) + +## [7.1.6](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.5...v7.1.6) (2023-04-13) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#260](https://github.com/libp2p/js-libp2p-mplex/issues/260)) ([c63ed58](https://github.com/libp2p/js-libp2p-mplex/commit/c63ed5843215353f9fdc81dffd5a635984f9d97c)) +* **dev:** bump it-foreach from 1.0.1 to 2.0.2 ([#265](https://github.com/libp2p/js-libp2p-mplex/issues/265)) ([76d27a4](https://github.com/libp2p/js-libp2p-mplex/commit/76d27a4b11f7b3d27d6b17f175ce9c5a72cda870)) +* **dev:** bump it-pipe from 2.0.5 to 3.0.1 ([#268](https://github.com/libp2p/js-libp2p-mplex/issues/268)) ([bd37717](https://github.com/libp2p/js-libp2p-mplex/commit/bd37717aa11ef68a25677645dadf6a75e99ae70b)) + +## [7.1.5](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.4...v7.1.5) (2023-04-13) + + +### Dependencies + +* update any-signal to 4.x.x ([#270](https://github.com/libp2p/js-libp2p-mplex/issues/270)) ([2820884](https://github.com/libp2p/js-libp2p-mplex/commit/2820884d005dfe44bf6009cf7a0c5d28bdc23f14)) + +## [7.1.4](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.3...v7.1.4) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#269](https://github.com/libp2p/js-libp2p-mplex/issues/269)) ([d14a122](https://github.com/libp2p/js-libp2p-mplex/commit/d14a122772680db171746acb9b639f3e7ffb46f4)) + +## [7.1.3](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.2...v7.1.3) (2023-03-31) + + +### Dependencies + +* **dev:** bump it-map from 2.0.1 to 3.0.1 ([#263](https://github.com/libp2p/js-libp2p-mplex/issues/263)) ([c438001](https://github.com/libp2p/js-libp2p-mplex/commit/c4380018b73a00ab360997524a3375b8210f91e8)) + +## [7.1.2](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.1...v7.1.2) (2023-03-21) + + +### Trivial Changes + +* replace err-code with CodeError ([#242](https://github.com/libp2p/js-libp2p-mplex/issues/242)) ([8d58a3b](https://github.com/libp2p/js-libp2p-mplex/commit/8d58a3b187aa174970251f6040fe82f9e5ff0c6d)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([54de88d](https://github.com/libp2p/js-libp2p-mplex/commit/54de88df5443b031c4d443ed23480229d3e89cbb)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([df03e8d](https://github.com/libp2p/js-libp2p-mplex/commit/df03e8df27fe52f4471b259ff65a14afc8d5d203)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([9c3f235](https://github.com/libp2p/js-libp2p-mplex/commit/9c3f235a7f2cb776b90387aaf53c57084e4de95f)) + + +### Dependencies + +* **dev:** bump typescript from 4.9.5 to 5.0.2 ([#256](https://github.com/libp2p/js-libp2p-mplex/issues/256)) ([a3590af](https://github.com/libp2p/js-libp2p-mplex/commit/a3590af093dee73c873aef1539bc8d81b0d61b08)) +* **dev:** Upgrade aegir to 38.1.7 ([#257](https://github.com/libp2p/js-libp2p-mplex/issues/257)) ([e0bf45a](https://github.com/libp2p/js-libp2p-mplex/commit/e0bf45af3ec85986d15ce3d05cb62637bc761a3e)) + +## [7.1.1](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.0...v7.1.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#241](https://github.com/libp2p/js-libp2p-mplex/issues/241)) ([083f462](https://github.com/libp2p/js-libp2p-mplex/commit/083f462b36429ae0f326b79c253017f824fba6ed)) + +## [7.1.0](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.7...v7.1.0) (2022-11-25) + + +### Features + +* add message byte batching ([#235](https://github.com/libp2p/js-libp2p-mplex/issues/235)) ([4e2a49d](https://github.com/libp2p/js-libp2p-mplex/commit/4e2a49df22430316140cd37a96fc3b6a8f95b76a)) + +## [7.0.7](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.6...v7.0.7) (2022-11-25) + + +### Bug Fixes + +* only accept lists of messages in encoder ([#236](https://github.com/libp2p/js-libp2p-mplex/issues/236)) ([4175cac](https://github.com/libp2p/js-libp2p-mplex/commit/4175cacbc32a76a525beccfad8fc13f87733e725)) + +## [7.0.6](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.5...v7.0.6) (2022-11-25) + + +### Bug Fixes + +* reduce async iterator loops per package in _createSink ([#224](https://github.com/libp2p/js-libp2p-mplex/issues/224)) ([e2a32ad](https://github.com/libp2p/js-libp2p-mplex/commit/e2a32ad1cc9bf396c95906e500c1f74acc134828)), closes [/github.com/libp2p/js-libp2p/issues/1420#issuecomment-1273272662](https://github.com/libp2p//github.com/libp2p/js-libp2p/issues/1420/issues/issuecomment-1273272662) + +## [7.0.5](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.4...v7.0.5) (2022-11-24) + + +### Bug Fixes + +* apply message size limit before decoding message ([#231](https://github.com/libp2p/js-libp2p-mplex/issues/231)) ([279ad47](https://github.com/libp2p/js-libp2p-mplex/commit/279ad47517ae3d4bc99ab499bf1fd9ef67dbb74b)) +* limit unprocessed message queue size separately to message size ([#234](https://github.com/libp2p/js-libp2p-mplex/issues/234)) ([2297856](https://github.com/libp2p/js-libp2p-mplex/commit/2297856c3ffb05f9cabf52efc3b78ef96d3faf1e)) +* yield single buffers ([#233](https://github.com/libp2p/js-libp2p-mplex/issues/233)) ([31d3938](https://github.com/libp2p/js-libp2p-mplex/commit/31d3938f8fcdf56debbf8824ccbcbc057d5bd5be)) + +## [7.0.4](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.3...v7.0.4) (2022-11-23) + + +### Dependencies + +* **dev:** bump it-map from 1.0.6 to 2.0.0 ([#225](https://github.com/libp2p/js-libp2p-mplex/issues/225)) ([a153108](https://github.com/libp2p/js-libp2p-mplex/commit/a15310817a325b5106112562d82739d86fa50a49)) + + +### Trivial Changes + +* update benchmark ([#232](https://github.com/libp2p/js-libp2p-mplex/issues/232)) ([d73381e](https://github.com/libp2p/js-libp2p-mplex/commit/d73381e00b5109505a15b10512e52d17b4b78dd6)) + +## [7.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.2...v7.0.3) (2022-11-23) + + +### Dependencies + +* **dev:** bump it-all from 1.0.6 to 2.0.0 ([#227](https://github.com/libp2p/js-libp2p-mplex/issues/227)) ([345b37d](https://github.com/libp2p/js-libp2p-mplex/commit/345b37d3668298ca7d55fbc7e7e12091add1a219)) +* **dev:** bump it-foreach from 0.1.1 to 1.0.0 ([#226](https://github.com/libp2p/js-libp2p-mplex/issues/226)) ([01bae35](https://github.com/libp2p/js-libp2p-mplex/commit/01bae35a5c41346a945990be2618385bbea79572)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.1...v7.0.2) (2022-11-23) + + +### Dependencies + +* **dev:** bump it-drain from 1.0.5 to 2.0.0 ([#228](https://github.com/libp2p/js-libp2p-mplex/issues/228)) ([263251f](https://github.com/libp2p/js-libp2p-mplex/commit/263251fe3a4b8f55f6b4f431bdbfdb5b1006b42a)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.0...v7.0.1) (2022-11-21) + + +### Bug Fixes + +* type errors ([#230](https://github.com/libp2p/js-libp2p-mplex/issues/230)) ([e9c390a](https://github.com/libp2p/js-libp2p-mplex/commit/e9c390a195c46718e31e1f3bd233b0ab7c1f76b0)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v6.0.2...v7.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#223](https://github.com/libp2p/js-libp2p-mplex/issues/223)) ([9c9497f](https://github.com/libp2p/js-libp2p-mplex/commit/9c9497f5cb7a7fbe095d44d508a57a458dae9129)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v6.0.1...v6.0.2) (2022-10-07) + + +### Dependencies + +* bump @libp2p/interface-stream-muxer from 2.0.2 to 3.0.0 ([#220](https://github.com/libp2p/js-libp2p-mplex/issues/220)) ([5b45249](https://github.com/libp2p/js-libp2p-mplex/commit/5b452497d1f294ddf7c74283a16ea9e12f98c438)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v6.0.0...v6.0.1) (2022-10-07) + + +### Dependencies + +* **dev:** bump @libp2p/interface-stream-muxer-compliance-tests from 4.0.0 to 5.0.0 ([#221](https://github.com/libp2p/js-libp2p-mplex/issues/221)) ([1e3153e](https://github.com/libp2p/js-libp2p-mplex/commit/1e3153e3da749ce159708e838bc350f9b024a32b)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.4...v6.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#222) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#222](https://github.com/libp2p/js-libp2p-mplex/issues/222)) ([9b7a800](https://github.com/libp2p/js-libp2p-mplex/commit/9b7a80003f87c76df4d069d3e0d29c940a9237b2)) + +## [5.2.4](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.3...v5.2.4) (2022-09-23) + + +### Bug Fixes + +* remove tracked map as the stats overwrite each other ([#217](https://github.com/libp2p/js-libp2p-mplex/issues/217)) ([d5f4d5f](https://github.com/libp2p/js-libp2p-mplex/commit/d5f4d5f6c92f2d6cc275d8a2250b1feb9b6b756f)) + + +### Trivial Changes + +* ignore coverage dir ([#219](https://github.com/libp2p/js-libp2p-mplex/issues/219)) ([298590f](https://github.com/libp2p/js-libp2p-mplex/commit/298590f839d147abc1f7168561a589b5977299ef)) +* refactor benchmarks for use in the browser ([#218](https://github.com/libp2p/js-libp2p-mplex/issues/218)) ([ccd3dc7](https://github.com/libp2p/js-libp2p-mplex/commit/ccd3dc74289273135345005c6e64e2a3b4e48be7)) + +## [5.2.3](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.2...v5.2.3) (2022-09-20) + + +### Bug Fixes + +* optimize stream sink for small messages ([#216](https://github.com/libp2p/js-libp2p-mplex/issues/216)) ([a10205b](https://github.com/libp2p/js-libp2p-mplex/commit/a10205bbf19db147a33201ba8fe1fc793661080d)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([cf94f56](https://github.com/libp2p/js-libp2p-mplex/commit/cf94f56d82457f1c40e5d715d378216bb7d0ed5a)) + +## [5.2.2](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.1...v5.2.2) (2022-09-12) + + +### Bug Fixes + +* chunk messages over maxMsgSize ([#214](https://github.com/libp2p/js-libp2p-mplex/issues/214)) ([6d2d8cc](https://github.com/libp2p/js-libp2p-mplex/commit/6d2d8ccf412b4937a955f567bd5f65705b9827a1)) + +## [5.2.1](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.0...v5.2.1) (2022-09-08) + + +### Bug Fixes + +* do not treat stream source as pushable ([#211](https://github.com/libp2p/js-libp2p-mplex/issues/211)) ([359c103](https://github.com/libp2p/js-libp2p-mplex/commit/359c1038b870f84a9dfd390d4a0021c157fe0bcc)) + +## [5.2.0](https://github.com/libp2p/js-libp2p-mplex/compare/v5.1.2...v5.2.0) (2022-09-07) + + +### Features + +* close connections when too many streams are opened ([#213](https://github.com/libp2p/js-libp2p-mplex/issues/213)) ([9140770](https://github.com/libp2p/js-libp2p-mplex/commit/9140770f4559677bbe69fc84d4d2dbcc70068c9e)) + +## [5.1.2](https://github.com/libp2p/js-libp2p-mplex/compare/v5.1.1...v5.1.2) (2022-09-07) + + +### Documentation + +* make the example work ([#206](https://github.com/libp2p/js-libp2p-mplex/issues/206)) ([f07acc3](https://github.com/libp2p/js-libp2p-mplex/commit/f07acc361a0627ae848804ed6eb422efad70878f)) + +## [5.1.1](https://github.com/libp2p/js-libp2p-mplex/compare/v5.1.0...v5.1.1) (2022-08-30) + + +### Dependencies + +* update it-pushable ([#210](https://github.com/libp2p/js-libp2p-mplex/issues/210)) ([1188272](https://github.com/libp2p/js-libp2p-mplex/commit/1188272df64c490449c1d3341e4c06c320116b30)), closes [#209](https://github.com/libp2p/js-libp2p-mplex/issues/209) + +## [5.1.0](https://github.com/libp2p/js-libp2p-mplex/compare/v5.0.0...v5.1.0) (2022-08-30) + + +### Features + +* add benchmark ([#207](https://github.com/libp2p/js-libp2p-mplex/issues/207)) ([6bf491f](https://github.com/libp2p/js-libp2p-mplex/commit/6bf491fdad73ee29849740754d5094bc85e26c78)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.3...v5.0.0) (2022-08-10) + + +### ⚠ BREAKING CHANGES + +* mulitplexed streams now emit `Uint8ArrayList`s and not `Uint8Array`s to handle the case for when transports have smaller chunk sizes than the multiplexer + +### Bug Fixes + +* emit uint8arraylists for data ([#201](https://github.com/libp2p/js-libp2p-mplex/issues/201)) ([e85ebab](https://github.com/libp2p/js-libp2p-mplex/commit/e85ebab233117643ba8b5acc33b7f90dc491f27d)) + +## [4.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.2...v4.0.3) (2022-08-03) + + +### Trivial Changes + +* update project config ([#197](https://github.com/libp2p/js-libp2p-mplex/issues/197)) ([46334e6](https://github.com/libp2p/js-libp2p-mplex/commit/46334e6859cd17c47fe3ffcf2f194eb00f3e748a)) + + +### Dependencies + +* update uint8arraylist dep ([#199](https://github.com/libp2p/js-libp2p-mplex/issues/199)) ([6e3b9d8](https://github.com/libp2p/js-libp2p-mplex/commit/6e3b9d8b38d283e62103322f1173ccfed4db5a6a)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.1...v4.0.2) (2022-07-25) + + +### Bug Fixes + +* remove MPLEX_ prefix from error codes ([#195](https://github.com/libp2p/js-libp2p-mplex/issues/195)) ([c6c9581](https://github.com/libp2p/js-libp2p-mplex/commit/c6c9581b34259e1d3811a2edb91a1cc1ef854364)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.0...v4.0.1) (2022-07-22) + + +### Bug Fixes + +* remove need of buffer polyfill config for browser ([#194](https://github.com/libp2p/js-libp2p-mplex/issues/194)) ([7c39830](https://github.com/libp2p/js-libp2p-mplex/commit/7c39830280347dbcf976a921f677e7b0e725b9f7)) +* reset stream when over inbound stream limit ([#193](https://github.com/libp2p/js-libp2p-mplex/issues/193)) ([41fefa4](https://github.com/libp2p/js-libp2p-mplex/commit/41fefa4280e122f553fed72ce5c81805755dcc35)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v3.0.0...v4.0.0) (2022-06-28) + + +### ⚠ BREAKING CHANGES + +* upgrade to interface-stream-muxer 2.0.0 (#186) + +### Bug Fixes + +* upgrade to interface-stream-muxer 2.0.0 ([#186](https://github.com/libp2p/js-libp2p-mplex/issues/186)) ([f11f2ce](https://github.com/libp2p/js-libp2p-mplex/commit/f11f2ce88f705d0836414fa3ddda1b08f046437c)), closes [#185](https://github.com/libp2p/js-libp2p-mplex/issues/185) + +## [3.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v2.0.0...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* updates to simplified connection interface + +### Bug Fixes + +* limit incoming and outgoing streams separately ([#184](https://github.com/libp2p/js-libp2p-mplex/issues/184)) ([cd55d36](https://github.com/libp2p/js-libp2p-mplex/commit/cd55d36d4245868ebb884f0ce69fc6dfa5d8ca4b)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v1.2.1...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#181](https://github.com/libp2p/js-libp2p-mplex/issues/181)) ([dcd02d9](https://github.com/libp2p/js-libp2p-mplex/commit/dcd02d9456f223c43062ac031c7a03aa6c635f30)) + +### [1.2.1](https://github.com/libp2p/js-libp2p-mplex/compare/v1.2.0...v1.2.1) (2022-06-13) + + +### Bug Fixes + +* fix typo in error message ([#177](https://github.com/libp2p/js-libp2p-mplex/issues/177)) ([f71119d](https://github.com/libp2p/js-libp2p-mplex/commit/f71119d640ac8f3721ad1a87a5c4ccc8fc4bda1d)) + +## [1.2.0](https://github.com/libp2p/js-libp2p-mplex/compare/v1.1.2...v1.2.0) (2022-06-13) + + +### Features + +* limit internal message buffer size ([#174](https://github.com/libp2p/js-libp2p-mplex/issues/174)) ([0c8e1b0](https://github.com/libp2p/js-libp2p-mplex/commit/0c8e1b06d31c46b6ef768139c822caac1904789d)), closes [/github.com/libp2p/go-mplex/blob/master/multiplex.go#L26](https://github.com/libp2p//github.com/libp2p/go-mplex/blob/master/multiplex.go/issues/L26) + +### [1.1.2](https://github.com/libp2p/js-libp2p-mplex/compare/v1.1.1...v1.1.2) (2022-06-08) + + +### Bug Fixes + +* add per-connection stream limit ([#173](https://github.com/libp2p/js-libp2p-mplex/issues/173)) ([21371e7](https://github.com/libp2p/js-libp2p-mplex/commit/21371e7251b1d5523d7e4e09afa9a2ea3daa8079)) + +### [1.1.1](https://github.com/libp2p/js-libp2p-mplex/compare/v1.1.0...v1.1.1) (2022-06-08) + + +### Bug Fixes + +* re-enable encode from Uint8ArrayList test ([#172](https://github.com/libp2p/js-libp2p-mplex/issues/172)) ([897031f](https://github.com/libp2p/js-libp2p-mplex/commit/897031fa79cf5b8c2a746228341c8d31169c2af9)) + +## [1.1.0](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.5...v1.1.0) (2022-05-23) + + +### Features + +* close read and write streams ([#170](https://github.com/libp2p/js-libp2p-mplex/issues/170)) ([3917968](https://github.com/libp2p/js-libp2p-mplex/commit/39179686ae033a2cc2821707dbec9e766fb4e099)), closes [#120](https://github.com/libp2p/js-libp2p-mplex/issues/120) [#115](https://github.com/libp2p/js-libp2p-mplex/issues/115) + +### [1.0.5](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.4...v1.0.5) (2022-05-05) + + +### Bug Fixes + +* ignore missing stream ([#169](https://github.com/libp2p/js-libp2p-mplex/issues/169)) ([f6d3dd9](https://github.com/libp2p/js-libp2p-mplex/commit/f6d3dd9f55020df93c8e7e116cb2ce1614b3404b)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.3...v1.0.4) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#168](https://github.com/libp2p/js-libp2p-mplex/issues/168)) ([f592f96](https://github.com/libp2p/js-libp2p-mplex/commit/f592f96adb6527da633fdc235890e32e53625906)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.2...v1.0.3) (2022-04-09) + + +### Trivial Changes + +* update aegir ([#167](https://github.com/libp2p/js-libp2p-mplex/issues/167)) ([0ef0c36](https://github.com/libp2p/js-libp2p-mplex/commit/0ef0c36f4d84d85ddbc06b725967bd9edac7a1cc)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.1...v1.0.2) (2022-03-17) + + +### Bug Fixes + +* update interfaces ([#162](https://github.com/libp2p/js-libp2p-mplex/issues/162)) ([ab9079c](https://github.com/libp2p/js-libp2p-mplex/commit/ab9079c26a5c98ea5487107e79bbf17ae9b34ad2)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.0...v1.0.1) (2022-02-21) + + +### Bug Fixes + +* update interfaces ([#160](https://github.com/libp2p/js-libp2p-mplex/issues/160)) ([43db1cb](https://github.com/libp2p/js-libp2p-mplex/commit/43db1cb61440859abc2cdefe5a9a362d0bf19497)) + + +### Trivial Changes + +* module name ([0137b94](https://github.com/libp2p/js-libp2p-mplex/commit/0137b9451e554a32d7e1f1c10eaacc00df225762)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.7...v1.0.0) (2022-02-14) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +Co-authored-by: Marin Petrunić + +### Features + +* convert to typescript ([#158](https://github.com/libp2p/js-libp2p-mplex/issues/158)) ([0cf727a](https://github.com/libp2p/js-libp2p-mplex/commit/0cf727ae101b3006400701b781d05a12eada59b7)) + +### [0.10.7](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.6...v0.10.7) (2022-01-14) + + +### Bug Fixes + +* remove abort controller dep ([#152](https://github.com/libp2p/js-libp2p-mplex/issues/152)) ([96943cb](https://github.com/libp2p/js-libp2p-mplex/commit/96943cb68bc01efffd7045f0c5a9a3ed978fbf0e)) + +### [0.10.6](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.5...v0.10.6) (2022-01-14) + + +### Trivial Changes + +* switch to unified ci ([#151](https://github.com/libp2p/js-libp2p-mplex/issues/151)) ([f14c349](https://github.com/libp2p/js-libp2p-mplex/commit/f14c34974c8b298179782f5ce3de93fb439fd764)) + +## [0.10.5](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.4...v0.10.5) (2021-12-07) + + +### Performance Improvements + +* do not call varint.decode() if buffer has 0 length ([#125](https://github.com/libp2p/js-libp2p-mplex/issues/125)) ([92f1727](https://github.com/libp2p/js-libp2p-mplex/commit/92f1727342c278a8dd025623cc4fe6cb265485e9)) + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.3...v0.10.4) (2021-07-08) + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.2...v0.10.3) (2021-04-16) + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.5...v0.10.2) (2021-01-29) + + +### Bug Fixes + +* ensure stream closes on abort or reset ([#116](https://github.com/libp2p/js-libp2p-mplex/issues/116)) ([77835b3](https://github.com/libp2p/js-libp2p-mplex/commit/77835b326fbce02e3a9bf92f0084d01e4e1d9cf9)) +* replace node buffers with uint8arrays ([#114](https://github.com/libp2p/js-libp2p-mplex/issues/114)) ([d005338](https://github.com/libp2p/js-libp2p-mplex/commit/d005338154b6882a22396e921ba4a38cc4e213fc)) + + +### BREAKING CHANGES + +* - All use of node Buffers has been replaced with Uint8Arrays + +* fix: keep allocUnsafe for node for performance + +Co-authored-by: Jacob Heun + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.0...v0.10.1) (2020-10-22) + + +### Bug Fixes + +* ensure stream closes on abort or reset ([#116](https://github.com/libp2p/js-libp2p-mplex/issues/116)) ([77835b3](https://github.com/libp2p/js-libp2p-mplex/commit/77835b326fbce02e3a9bf92f0084d01e4e1d9cf9)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.5...v0.10.0) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#114](https://github.com/libp2p/js-libp2p-mplex/issues/114)) ([d005338](https://github.com/libp2p/js-libp2p-mplex/commit/d005338)) + + +### BREAKING CHANGES + +* - All use of node Buffers has been replaced with Uint8Arrays + +* fix: keep allocUnsafe for node for performance + +Co-authored-by: Jacob Heun + + + + +## [0.9.5](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.4...v0.9.5) (2020-03-18) + + +### Bug Fixes + +* add buffer ([#106](https://github.com/libp2p/js-libp2p-mplex/issues/106)) ([71f3e5b](https://github.com/libp2p/js-libp2p-mplex/commit/71f3e5b)) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.3...v0.9.4) (2020-02-13) + + +### Performance Improvements + +* small bl ([#101](https://github.com/libp2p/js-libp2p-mplex/issues/101)) ([7da79b6](https://github.com/libp2p/js-libp2p-mplex/commit/7da79b6)) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.2...v0.9.3) (2019-11-28) + + +### Features + +* message splitting ([#100](https://github.com/libp2p/js-libp2p-mplex/issues/100)) ([fba56a5](https://github.com/libp2p/js-libp2p-mplex/commit/fba56a5)) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.1...v0.9.2) (2019-10-28) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.0...v0.9.1) (2019-09-23) + + +### Features + +* add better support for external stream metadata tracking ([#98](https://github.com/libp2p/js-libp2p-mplex/issues/98)) ([96f1ca0](https://github.com/libp2p/js-libp2p-mplex/commit/96f1ca0)) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.5...v0.9.0) (2019-09-18) + + +### Code Refactoring + +* async iterators ([#94](https://github.com/libp2p/js-libp2p-mplex/issues/94)) ([c9bede5](https://github.com/libp2p/js-libp2p-mplex/commit/c9bede5)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await while pull-streams are replaced with async iterators. The API has also been updated according to the latest `interface-stream-muxer` version, https://github.com/libp2p/interface-stream-muxer/tree/v0.7.0. + +License: MIT +Signed-off-by: Alan Shaw + + + + +## [0.8.5](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.4...v0.8.5) (2019-03-18) + + + + +## [0.8.4](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.3...v0.8.4) (2018-11-15) + + + + +## [0.8.3](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.2...v0.8.3) (2018-11-08) + + +### Bug Fixes + +* muxer.end will no longer hang ([#86](https://github.com/libp2p/js-libp2p-mplex/issues/86)) ([e23cbaf](https://github.com/libp2p/js-libp2p-mplex/commit/e23cbaf)) + + + + +## [0.8.2](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.1...v0.8.2) (2018-10-01) + + +### Bug Fixes + +* improve resiliency of internals _send ([#84](https://github.com/libp2p/js-libp2p-mplex/issues/84)) ([70dafb7](https://github.com/libp2p/js-libp2p-mplex/commit/70dafb7)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.0...v0.8.1) (2018-10-01) + + +### Bug Fixes + +* verify drain before new push ([#82](https://github.com/libp2p/js-libp2p-mplex/issues/82)) ([cd77e01](https://github.com/libp2p/js-libp2p-mplex/commit/cd77e01)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.7.0...v0.8.0) (2018-06-19) + + +### Bug Fixes + +* add setImmediatte to the call of callback ([8cdcd0d](https://github.com/libp2p/js-libp2p-mplex/commit/8cdcd0d)) +* catch Multiplexer is destroyed error into callback ([#79](https://github.com/libp2p/js-libp2p-mplex/issues/79)) ([b60205f](https://github.com/libp2p/js-libp2p-mplex/commit/b60205f)) +* missing dep and readme example ([#77](https://github.com/libp2p/js-libp2p-mplex/issues/77)) ([904cd7c](https://github.com/libp2p/js-libp2p-mplex/commit/904cd7c)) +* package.json deps semver ([126b966](https://github.com/libp2p/js-libp2p-mplex/commit/126b966)) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.6.0...v0.7.0) (2018-04-05) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.5.1...v0.6.0) (2018-02-19) + + +### Features + +* mplex is all here ([20cf80a](https://github.com/libp2p/js-libp2p-mplex/commit/20cf80a)) +* support new Buffer ([c1384c3](https://github.com/libp2p/js-libp2p-mplex/commit/c1384c3)) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.5.0...v0.5.1) (2017-12-14) + + +### Features + +* porting to new aegir ([#70](https://github.com/libp2p/js-libp2p-multiplex/issues/70)) ([30fc825](https://github.com/libp2p/js-libp2p-multiplex/commit/30fc825)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.4...v0.5.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#69](https://github.com/libp2p/js-libp2p-multiplex/issues/69)) ([d58f50e](https://github.com/libp2p/js-libp2p-multiplex/commit/d58f50e)) + + + + +## [0.4.4](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.3...v0.4.4) (2017-07-08) + + + + +## [0.4.3](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.2...v0.4.3) (2017-03-21) + + + + +## [0.4.2](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.1...v0.4.2) (2017-03-21) + + +### Bug Fixes + +* add missing setImmediate shim ([b039b81](https://github.com/libp2p/js-libp2p-multiplex/commit/b039b81)), closes [#61](https://github.com/libp2p/js-libp2p-multiplex/issues/61) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.0...v0.4.1) (2017-02-21) + + +### Bug Fixes + +* correct handling of multiplex options ([fa78df4](https://github.com/libp2p/js-libp2p-multiplex/commit/fa78df4)) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.6...v0.4.0) (2017-02-15) + + + + +## [0.3.6](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.5...v0.3.6) (2017-02-09) + + + + +## [0.3.5](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.4...v0.3.5) (2017-01-26) + + + + +## [0.3.4](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.3...v0.3.4) (2017-01-24) + + + + +## [0.3.3](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.2...v0.3.3) (2017-01-24) + + +### Bug Fixes + +* check for callbacks ([9ef5553](https://github.com/libp2p/js-libp2p-multiplex/commit/9ef5553)) + + + + +## [0.3.2](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.1...v0.3.2) (2017-01-24) + + +### Bug Fixes + +* dropped packed ([a7cfb8b](https://github.com/libp2p/js-libp2p-multiplex/commit/a7cfb8b)) + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.0...v0.3.1) (2017-01-20) + + +### Bug Fixes + +* **docs:** Update readme.md's example and added files for it ([ccd94c8](https://github.com/libp2p/js-libp2p-multiplex/commit/ccd94c8)) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.2.1...v0.3.0) (2017-01-20) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.2.0...v0.2.1) (2016-03-22) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.1.0...v0.2.0) (2016-03-07) + + + + +# 0.1.0 (2016-03-07) diff --git a/packages/stream-multiplexer-mplex/LICENSE b/packages/stream-multiplexer-mplex/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/stream-multiplexer-mplex/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/stream-multiplexer-mplex/LICENSE-APACHE b/packages/stream-multiplexer-mplex/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/stream-multiplexer-mplex/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/stream-multiplexer-mplex/LICENSE-MIT b/packages/stream-multiplexer-mplex/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/stream-multiplexer-mplex/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/stream-multiplexer-mplex/README.md b/packages/stream-multiplexer-mplex/README.md new file mode 100644 index 0000000000..ee8b856784 --- /dev/null +++ b/packages/stream-multiplexer-mplex/README.md @@ -0,0 +1,73 @@ +# @libp2p/mplex + +[![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 + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +[![](https://github.com/libp2p/interface-stream-muxer/raw/master/img/badge.png)](https://github.com/libp2p/interface-stream-muxer) + +## Usage + +```js +import { mplex } from '@libp2p/mplex' +import { pipe } from 'it-pipe' + +const factory = mplex() + +const muxer = factory.createStreamMuxer(components, { + onStream: stream => { // Receive a duplex stream from the remote + // ...receive data from the remote and optionally send data back + }, + onStreamEnd: stream => { + // ...handle any tracking you may need of stream closures + } +}) + +pipe(conn, muxer, conn) // conn is duplex connection to another peer + +const stream = muxer.newStream() // Create a new duplex stream to the remote + +// Use the duplex stream to send some data to the remote... +pipe([1, 2, 3], stream) +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/stream-multiplexer-mplex/benchmark/send-and-receive.js b/packages/stream-multiplexer-mplex/benchmark/send-and-receive.js new file mode 100644 index 0000000000..cb247896c0 --- /dev/null +++ b/packages/stream-multiplexer-mplex/benchmark/send-and-receive.js @@ -0,0 +1,71 @@ +/* eslint-disable no-console */ + +/* +$ node benchmark/send-and-receive.js +$ npx playwright-test benchmark/send-and-receive.js --runner benchmark +*/ + +import Benchmark from 'benchmark' +import { pipe } from 'it-pipe' +import { expect } from 'aegir/chai' +import { pushable } from 'it-pushable' +import { mplex } from '../dist/src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +const factory = mplex()() +const muxer = factory.createStreamMuxer() +const stream1 = muxer.newStream('hello') +const muxer2 = factory.createStreamMuxer({ + onIncomingStream: async (stream) => { + await pipe( + stream, + async function * transform (source) { // A generator is async iterable + for await (const chunk of source) { + yield chunk + } + }, + stream + ) + } +}) + +pipe(muxer, muxer2, muxer) + +const p = pushable() +const promise = pipe(p, stream1, async function collect (source) { + const vals = [] + for await (const val of source) { + vals.push(val) + } + return vals +}) + +// typical data of ethereum consensus attestation +const data = uint8ArrayFromString( + 'e40000000a000000000000000a00000000000000a45c8daa336e17a150300afd4c717313c84f291754c51a378f20958083c5fa070a00000000000000a45c8daa336e17a150300afd4c717313c84f291754c51a378f20958083c5fa070a00000000000000a45c8daa336e17a150300afd4c717313c84f291754c51a378f20958083c5fa0795d2ef8ae4e2b4d1e5b3d5ce47b518e3db2c8c4d082e4498805ac2a686c69f248761b78437db2927470c1e77ede9c18606110faacbcbe4f13052bde7f7eff6aab09edf7bc4929fda2230f943aba2c47b6f940d350cb20c76fad4a8d40e2f3f1f01', + 'hex' +) + +const count = 1000 + +new Benchmark.Suite() + .add('send and receive', async () => { + for (let i = 0; i < count; i++) { + p.push(data) + } + p.end() + const arr = await promise + expect(arr.length).to.be.equal(count) + }) + .on('error', (err) => { + console.error(err) + }) + .on('cycle', (event) => { + console.info(String(event.target)) + }) + .on('complete', function () { + // @ts-expect-error types are wrong + console.info(`Fastest is ${this.filter('fastest').map('name')}`) // eslint-disable-line @typescript-eslint/restrict-template-expressions + }) + // run async + .run({ async: true }) diff --git a/packages/stream-multiplexer-mplex/examples/dialer.js b/packages/stream-multiplexer-mplex/examples/dialer.js new file mode 100644 index 0000000000..9c33ba34ba --- /dev/null +++ b/packages/stream-multiplexer-mplex/examples/dialer.js @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ +'use strict' + +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import tcp from 'net' +import { pipe } from 'it-pipe' +import { toIterable } from './util.js' +import { Mplex } from '../dist/src/index.js' + +const socket = toIterable(tcp.connect(9999)) +console.log('[dialer] socket stream opened') + +const controller = new AbortController() + +const factory = new Mplex({ signal: controller.signal }) +const muxer = factory.createStreamMuxer() + +const pipeMuxerToSocket = async () => { + await pipe(muxer, socket, muxer) + console.log('[dialer] socket stream closed') +} + +const sendAndReceive = async () => { + const muxedStream = muxer.newStream('hello') + console.log('[dialer] muxed stream opened') + + await pipe( + [uint8ArrayFromString('hey, how is it going. I am the dialer')], + muxedStream, + async source => { + for await (const chunk of source) { + console.log('[dialer] received:') + console.log(uint8ArrayToString(chunk.slice())) + } + } + ) + console.log('[dialer] muxed stream closed') + + // Close the socket stream after 1s + setTimeout(() => controller.abort(), 1000) +} + +pipeMuxerToSocket() +sendAndReceive() diff --git a/packages/stream-multiplexer-mplex/examples/listener.js b/packages/stream-multiplexer-mplex/examples/listener.js new file mode 100644 index 0000000000..189c945531 --- /dev/null +++ b/packages/stream-multiplexer-mplex/examples/listener.js @@ -0,0 +1,37 @@ +/* eslint-disable no-console */ +'use strict' + +import tcp from 'net' +import { pipe } from 'it-pipe' +import { toIterable } from './util.js' +import { Mplex } from '../dist/src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +const listener = tcp.createServer(async socket => { + console.log('[listener] Got connection!') + + const factory = new Mplex() + socket = toIterable(socket) + const muxer = factory.createStreamMuxer({ + onIncomingStream: async (stream) => { + console.log('[listener] muxed stream opened, id:', stream.id) + await pipe( + stream, + source => (async function * () { + for await (const chunk of source) { + console.log('[listener] received:') + console.log(uint8ArrayToString(chunk.slice())) + yield uint8ArrayFromString('thanks for the message, I am the listener') + } + })(), + stream + ) + console.log('[listener] muxed stream closed') + } + }) + await pipe(socket, muxer, socket) + console.log('[listener] socket stream closed') +}) + +listener.listen(9999, () => console.log('[listener] listening on 9999')) diff --git a/packages/stream-multiplexer-mplex/examples/util.js b/packages/stream-multiplexer-mplex/examples/util.js new file mode 100644 index 0000000000..057c0c6787 --- /dev/null +++ b/packages/stream-multiplexer-mplex/examples/util.js @@ -0,0 +1,17 @@ +// Simple convertion of Node.js duplex to iterable duplex (no backpressure) +export const toIterable = socket => { + return { + sink: async source => { + try { + for await (const chunk of source) { + socket.write(chunk) + } + } catch (err) { + // If not an abort then destroy the socket with an error + return socket.destroy(err.code === 'ABORT_ERR' ? null : err) + } + socket.end() + }, + source: socket + } +} diff --git a/packages/stream-multiplexer-mplex/package.json b/packages/stream-multiplexer-mplex/package.json new file mode 100644 index 0000000000..aaafc96a07 --- /dev/null +++ b/packages/stream-multiplexer-mplex/package.json @@ -0,0 +1,96 @@ +{ + "name": "@libp2p/mplex", + "version": "8.0.4", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "connection", + "duplex", + "libp2p", + "mplex", + "multiplex", + "muxer", + "stream" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "benchmark": "node ./node_modules/.bin/benchmark benchmark/send-and-receive.js", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-stream-muxer": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "abortable-iterator": "^5.0.1", + "any-signal": "^4.1.1", + "benchmark": "^2.1.4", + "it-batched-bytes": "^2.0.2", + "it-pushable": "^3.1.3", + "it-stream-types": "^2.0.1", + "rate-limiter-flexible": "^2.3.11", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3", + "varint": "^6.0.0" + }, + "devDependencies": { + "@libp2p/interface-stream-muxer-compliance-tests": "^7.0.0", + "@types/varint": "^6.0.0", + "aegir": "^39.0.10", + "cborg": "^2.0.1", + "delay": "^6.0.0", + "iso-random-stream": "^2.0.2", + "it-all": "^3.0.1", + "it-drain": "^3.0.2", + "it-foreach": "^2.0.2", + "it-map": "^3.0.3", + "it-pipe": "^3.0.1", + "it-to-buffer": "^4.0.1", + "p-defer": "^4.0.0", + "random-int": "^3.0.0" + }, + "browser": { + "./dist/src/alloc-unsafe.js": "./dist/src/alloc-unsafe-browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts b/packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts new file mode 100644 index 0000000000..111defb5c6 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts @@ -0,0 +1,3 @@ +export function allocUnsafe (size: number): Uint8Array { + return new Uint8Array(size) +} diff --git a/packages/stream-multiplexer-mplex/src/alloc-unsafe.ts b/packages/stream-multiplexer-mplex/src/alloc-unsafe.ts new file mode 100644 index 0000000000..5387f4153c --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/alloc-unsafe.ts @@ -0,0 +1,3 @@ +export function allocUnsafe (size: number): Buffer { + return Buffer.allocUnsafe(size) +} diff --git a/packages/stream-multiplexer-mplex/src/decode.ts b/packages/stream-multiplexer-mplex/src/decode.ts new file mode 100644 index 0000000000..2b85cd5e83 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/decode.ts @@ -0,0 +1,142 @@ +import { Uint8ArrayList } from 'uint8arraylist' +import { MessageTypeNames, MessageTypes } from './message-types.js' +import type { Message } from './message-types.js' + +export const MAX_MSG_SIZE = 1 << 20 // 1MB +export const MAX_MSG_QUEUE_SIZE = 4 << 20 // 4MB + +interface MessageHeader { + id: number + type: keyof typeof MessageTypeNames + offset: number + length: number +} + +export class Decoder { + private readonly _buffer: Uint8ArrayList + private _headerInfo: MessageHeader | null + private readonly _maxMessageSize: number + private readonly _maxUnprocessedMessageQueueSize: number + + constructor (maxMessageSize: number = MAX_MSG_SIZE, maxUnprocessedMessageQueueSize: number = MAX_MSG_QUEUE_SIZE) { + this._buffer = new Uint8ArrayList() + this._headerInfo = null + this._maxMessageSize = maxMessageSize + this._maxUnprocessedMessageQueueSize = maxUnprocessedMessageQueueSize + } + + write (chunk: Uint8Array | Uint8ArrayList): Message[] { + if (chunk == null || chunk.length === 0) { + return [] + } + + this._buffer.append(chunk) + + if (this._buffer.byteLength > this._maxUnprocessedMessageQueueSize) { + throw Object.assign(new Error('unprocessed message queue size too large!'), { code: 'ERR_MSG_QUEUE_TOO_BIG' }) + } + + const msgs: Message[] = [] + + while (this._buffer.length !== 0) { + if (this._headerInfo == null) { + try { + this._headerInfo = this._decodeHeader(this._buffer) + } catch (err: any) { + if (err.code === 'ERR_MSG_TOO_BIG') { + throw err + } + + break // We haven't received enough data yet + } + } + + const { id, type, length, offset } = this._headerInfo + const bufferedDataLength = this._buffer.length - offset + + if (bufferedDataLength < length) { + break // not enough data yet + } + + const msg: any = { + id, + type + } + + if (type === MessageTypes.NEW_STREAM || type === MessageTypes.MESSAGE_INITIATOR || type === MessageTypes.MESSAGE_RECEIVER) { + msg.data = this._buffer.sublist(offset, offset + length) + } + + msgs.push(msg) + + this._buffer.consume(offset + length) + this._headerInfo = null + } + + return msgs + } + + /** + * Attempts to decode the message header from the buffer + */ + _decodeHeader (data: Uint8ArrayList): MessageHeader { + const { + value: h, + offset + } = readVarInt(data) + const { + value: length, + offset: end + } = readVarInt(data, offset) + + const type = h & 7 + + // @ts-expect-error h is a number not a CODE + if (MessageTypeNames[type] == null) { + throw new Error(`Invalid type received: ${type}`) + } + + // test message type varint + data length + if (length > this._maxMessageSize) { + throw Object.assign(new Error('message size too large!'), { code: 'ERR_MSG_TOO_BIG' }) + } + + // @ts-expect-error h is a number not a CODE + return { id: h >> 3, type, offset: offset + end, length } + } +} + +const MSB = 0x80 +const REST = 0x7F + +export interface ReadVarIntResult { + value: number + offset: number +} + +function readVarInt (buf: Uint8ArrayList, offset: number = 0): ReadVarIntResult { + let res = 0 + let shift = 0 + let counter = offset + let b: number + const l = buf.length + + do { + if (counter >= l || shift > 49) { + offset = 0 + throw new RangeError('Could not decode varint') + } + b = buf.get(counter++) + res += shift < 28 + ? (b & REST) << shift + : (b & REST) * Math.pow(2, shift) + shift += 7 + } while (b >= MSB) + + offset = counter - offset + + return { + value: res, + offset + } +} diff --git a/packages/stream-multiplexer-mplex/src/encode.ts b/packages/stream-multiplexer-mplex/src/encode.ts new file mode 100644 index 0000000000..9dc5194fdd --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/encode.ts @@ -0,0 +1,84 @@ +import batchedBytes from 'it-batched-bytes' +import { Uint8ArrayList } from 'uint8arraylist' +import varint from 'varint' +import { allocUnsafe } from './alloc-unsafe.js' +import { type Message, MessageTypes } from './message-types.js' +import type { Source } from 'it-stream-types' + +const POOL_SIZE = 10 * 1024 + +class Encoder { + private _pool: Uint8Array + private _poolOffset: number + + constructor () { + this._pool = allocUnsafe(POOL_SIZE) + this._poolOffset = 0 + } + + /** + * Encodes the given message and adds it to the passed list + */ + write (msg: Message, list: Uint8ArrayList): void { + const pool = this._pool + let offset = this._poolOffset + + varint.encode(msg.id << 3 | msg.type, pool, offset) + offset += varint.encode.bytes ?? 0 + + if ((msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) && msg.data != null) { + varint.encode(msg.data.length, pool, offset) + } else { + varint.encode(0, pool, offset) + } + + offset += varint.encode.bytes ?? 0 + + const header = pool.subarray(this._poolOffset, offset) + + if (POOL_SIZE - offset < 100) { + this._pool = allocUnsafe(POOL_SIZE) + this._poolOffset = 0 + } else { + this._poolOffset = offset + } + + list.append(header) + + if ((msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) && msg.data != null) { + list.append(msg.data) + } + } +} + +const encoder = new Encoder() + +/** + * Encode and yield one or more messages + */ +export async function * encode (source: Source, minSendBytes: number = 0): AsyncGenerator { + if (minSendBytes == null || minSendBytes === 0) { + // just send the messages + for await (const messages of source) { + const list = new Uint8ArrayList() + + for (const msg of messages) { + encoder.write(msg, list) + } + + yield list.subarray() + } + + return + } + + // batch messages up for sending + yield * batchedBytes(source, { + size: minSendBytes, + serialize: (obj, list) => { + for (const m of obj) { + encoder.write(m, list) + } + } + }) +} diff --git a/packages/stream-multiplexer-mplex/src/index.ts b/packages/stream-multiplexer-mplex/src/index.ts new file mode 100644 index 0000000000..d78f65255d --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/index.ts @@ -0,0 +1,81 @@ +import { MplexStreamMuxer } from './mplex.js' +import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface-stream-muxer' + +export interface MplexInit { + /** + * The maximum size of message that can be sent in one go in bytes. + * Messages larger than this will be split into multiple smaller + * messages. If we receive a message larger than this an error will + * be thrown and the connection closed. (default: 1MB) + */ + maxMsgSize?: number + + /** + * Constrains the size of the unprocessed message queue buffer. + * Before messages are deserialized, the raw bytes are buffered to ensure + * we have the complete message to deserialized. If the queue gets longer + * than this value an error will be thrown and the connection closed. + * (default: 4MB) + */ + maxUnprocessedMessageQueueSize?: number + + /** + * Each byte array written into a multiplexed stream is converted to one or + * more messages which are sent as byte arrays to the remote node. Sending + * lots of small messages can be expensive - use this setting to batch up + * the serialized bytes of all messages sent during the current tick up to + * this limit to send in one go similar to Nagle's algorithm. N.b. you + * should benchmark your application carefully when using this setting as it + * may cause the opposite of the desired effect. Omit this setting to send + * all messages as they become available. (default: undefined) + */ + minSendBytes?: number + + /** + * The maximum number of multiplexed streams that can be open at any + * one time. A request to open more than this will have a stream + * reset message sent immediately as a response for the newly opened + * stream id (default: 1024) + */ + maxInboundStreams?: number + + /** + * The maximum number of multiplexed streams that can be open at any + * one time. An attempt to open more than this will throw (default: 1024) + */ + maxOutboundStreams?: number + + /** + * Incoming stream messages are buffered until processed by the stream + * handler. If the buffer reaches this size in bytes the stream will + * be reset (default: 4MB) + */ + maxStreamBufferSize?: number + + /** + * When `maxInboundStreams` is hit, if the remote continues try to open + * more than this many new multiplexed streams per second the connection + * will be closed (default: 5) + */ + disconnectThreshold?: number +} + +class Mplex implements StreamMuxerFactory { + public protocol = '/mplex/6.7.0' + private readonly _init: MplexInit + + constructor (init: MplexInit = {}) { + this._init = init + } + + createStreamMuxer (init: StreamMuxerInit = {}): StreamMuxer { + return new MplexStreamMuxer({ + ...init, + ...this._init + }) + } +} + +export function mplex (init: MplexInit = {}): () => StreamMuxerFactory { + return () => new Mplex(init) +} diff --git a/packages/stream-multiplexer-mplex/src/message-types.ts b/packages/stream-multiplexer-mplex/src/message-types.ts new file mode 100644 index 0000000000..852acb7eac --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/message-types.ts @@ -0,0 +1,79 @@ +import type { Uint8ArrayList } from 'uint8arraylist' + +type INITIATOR_NAME = 'NEW_STREAM' | 'MESSAGE' | 'CLOSE' | 'RESET' +type RECEIVER_NAME = 'MESSAGE' | 'CLOSE' | 'RESET' +type NAME = 'NEW_STREAM' | 'MESSAGE_INITIATOR' | 'CLOSE_INITIATOR' | 'RESET_INITIATOR' | 'MESSAGE_RECEIVER' | 'CLOSE_RECEIVER' | 'RESET_RECEIVER' +type CODE = 0 | 1 | 2 | 3 | 4 | 5 | 6 + +export enum MessageTypes { + NEW_STREAM = 0, + MESSAGE_RECEIVER = 1, + MESSAGE_INITIATOR = 2, + CLOSE_RECEIVER = 3, + CLOSE_INITIATOR = 4, + RESET_RECEIVER = 5, + RESET_INITIATOR = 6 +} + +export const MessageTypeNames: Record = Object.freeze({ + 0: 'NEW_STREAM', + 1: 'MESSAGE_RECEIVER', + 2: 'MESSAGE_INITIATOR', + 3: 'CLOSE_RECEIVER', + 4: 'CLOSE_INITIATOR', + 5: 'RESET_RECEIVER', + 6: 'RESET_INITIATOR' +}) + +export const InitiatorMessageTypes: Record = Object.freeze({ + NEW_STREAM: MessageTypes.NEW_STREAM, + MESSAGE: MessageTypes.MESSAGE_INITIATOR, + CLOSE: MessageTypes.CLOSE_INITIATOR, + RESET: MessageTypes.RESET_INITIATOR +}) + +export const ReceiverMessageTypes: Record = Object.freeze({ + MESSAGE: MessageTypes.MESSAGE_RECEIVER, + CLOSE: MessageTypes.CLOSE_RECEIVER, + RESET: MessageTypes.RESET_RECEIVER +}) + +export interface NewStreamMessage { + id: number + type: MessageTypes.NEW_STREAM + data: Uint8ArrayList +} + +export interface MessageReceiverMessage { + id: number + type: MessageTypes.MESSAGE_RECEIVER + data: Uint8ArrayList +} + +export interface MessageInitiatorMessage { + id: number + type: MessageTypes.MESSAGE_INITIATOR + data: Uint8ArrayList +} + +export interface CloseReceiverMessage { + id: number + type: MessageTypes.CLOSE_RECEIVER +} + +export interface CloseInitiatorMessage { + id: number + type: MessageTypes.CLOSE_INITIATOR +} + +export interface ResetReceiverMessage { + id: number + type: MessageTypes.RESET_RECEIVER +} + +export interface ResetInitiatorMessage { + id: number + type: MessageTypes.RESET_INITIATOR +} + +export type Message = NewStreamMessage | MessageReceiverMessage | MessageInitiatorMessage | CloseReceiverMessage | CloseInitiatorMessage | ResetReceiverMessage | ResetInitiatorMessage diff --git a/packages/stream-multiplexer-mplex/src/mplex.ts b/packages/stream-multiplexer-mplex/src/mplex.ts new file mode 100644 index 0000000000..7b216711a7 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/mplex.ts @@ -0,0 +1,328 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import { anySignal } from 'any-signal' +import { pushableV } from 'it-pushable' +import { RateLimiterMemory } from 'rate-limiter-flexible' +import { toString as uint8ArrayToString } from 'uint8arrays' +import { Decoder } from './decode.js' +import { encode } from './encode.js' +import { MessageTypes, MessageTypeNames, type Message } from './message-types.js' +import { createStream } from './stream.js' +import type { MplexInit } from './index.js' +import type { Stream } from '@libp2p/interface-connection' +import type { StreamMuxer, StreamMuxerInit } from '@libp2p/interface-stream-muxer' +import type { Sink, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +const log = logger('libp2p:mplex') + +const MAX_STREAMS_INBOUND_STREAMS_PER_CONNECTION = 1024 +const MAX_STREAMS_OUTBOUND_STREAMS_PER_CONNECTION = 1024 +const MAX_STREAM_BUFFER_SIZE = 1024 * 1024 * 4 // 4MB +const DISCONNECT_THRESHOLD = 5 + +function printMessage (msg: Message): any { + const output: any = { + ...msg, + type: `${MessageTypeNames[msg.type]} (${msg.type})` + } + + if (msg.type === MessageTypes.NEW_STREAM) { + output.data = uint8ArrayToString(msg.data instanceof Uint8Array ? msg.data : msg.data.subarray()) + } + + if (msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) { + output.data = uint8ArrayToString(msg.data instanceof Uint8Array ? msg.data : msg.data.subarray(), 'base16') + } + + return output +} + +export interface MplexStream extends Stream { + sourceReadableLength: () => number + sourcePush: (data: Uint8ArrayList) => void +} + +interface MplexStreamMuxerInit extends MplexInit, StreamMuxerInit {} + +export class MplexStreamMuxer implements StreamMuxer { + public protocol = '/mplex/6.7.0' + + public sink: Sink, Promise> + public source: AsyncGenerator + + private _streamId: number + private readonly _streams: { initiators: Map, receivers: Map } + private readonly _init: MplexStreamMuxerInit + private readonly _source: { push: (val: Message) => void, end: (err?: Error) => void } + private readonly closeController: AbortController + private readonly rateLimiter: RateLimiterMemory + + constructor (init?: MplexStreamMuxerInit) { + init = init ?? {} + + this._streamId = 0 + this._streams = { + /** + * Stream to ids map + */ + initiators: new Map(), + /** + * Stream to ids map + */ + receivers: new Map() + } + this._init = init + + /** + * An iterable sink + */ + this.sink = this._createSink() + + /** + * An iterable source + */ + const source = this._createSource() + this._source = source + this.source = source + + /** + * Close controller + */ + this.closeController = new AbortController() + + this.rateLimiter = new RateLimiterMemory({ + points: init.disconnectThreshold ?? DISCONNECT_THRESHOLD, + duration: 1 + }) + } + + /** + * Returns a Map of streams and their ids + */ + get streams (): Stream[] { + // Inbound and Outbound streams may have the same ids, so we need to make those unique + const streams: Stream[] = [] + for (const stream of this._streams.initiators.values()) { + streams.push(stream) + } + + for (const stream of this._streams.receivers.values()) { + streams.push(stream) + } + return streams + } + + /** + * Initiate a new stream with the given name. If no name is + * provided, the id of the stream will be used. + */ + newStream (name?: string): Stream { + if (this.closeController.signal.aborted) { + throw new Error('Muxer already closed') + } + const id = this._streamId++ + name = name == null ? id.toString() : name.toString() + const registry = this._streams.initiators + return this._newStream({ id, name, type: 'initiator', registry }) + } + + /** + * Close or abort all tracked streams and stop the muxer + */ + close (err?: Error | undefined): void { + if (this.closeController.signal.aborted) return + + if (err != null) { + this.streams.forEach(s => { s.abort(err) }) + } else { + this.streams.forEach(s => { s.close() }) + } + this.closeController.abort() + } + + /** + * Called whenever an inbound stream is created + */ + _newReceiverStream (options: { id: number, name: string }): MplexStream { + const { id, name } = options + const registry = this._streams.receivers + return this._newStream({ id, name, type: 'receiver', registry }) + } + + _newStream (options: { id: number, name: string, type: 'initiator' | 'receiver', registry: Map }): MplexStream { + const { id, name, type, registry } = options + + log('new %s stream %s', type, id) + + if (type === 'initiator' && this._streams.initiators.size === (this._init.maxOutboundStreams ?? MAX_STREAMS_OUTBOUND_STREAMS_PER_CONNECTION)) { + throw new CodeError('Too many outbound streams open', 'ERR_TOO_MANY_OUTBOUND_STREAMS') + } + + if (registry.has(id)) { + throw new Error(`${type} stream ${id} already exists!`) + } + + const send = (msg: Message): void => { + if (log.enabled) { + log.trace('%s stream %s send', type, id, printMessage(msg)) + } + + this._source.push(msg) + } + + const onEnd = (): void => { + log('%s stream with id %s and protocol %s ended', type, id, stream.stat.protocol) + registry.delete(id) + + if (this._init.onStreamEnd != null) { + this._init.onStreamEnd(stream) + } + } + + const stream = createStream({ id, name, send, type, onEnd, maxMsgSize: this._init.maxMsgSize }) + registry.set(id, stream) + return stream + } + + /** + * Creates a sink with an abortable source. Incoming messages will + * also have their size restricted. All messages will be varint decoded. + */ + _createSink (): Sink, Promise> { + const sink: Sink, Promise> = async source => { + const signal = anySignal([this.closeController.signal, this._init.signal]) + + try { + source = abortableSource(source, signal) + + const decoder = new Decoder(this._init.maxMsgSize, this._init.maxUnprocessedMessageQueueSize) + + for await (const chunk of source) { + for (const msg of decoder.write(chunk)) { + await this._handleIncoming(msg) + } + } + + this._source.end() + } catch (err: any) { + log('error in sink', err) + this._source.end(err) // End the source with an error + } finally { + signal.clear() + } + } + + return sink + } + + /** + * Creates a source that restricts outgoing message sizes + * and varint encodes them + */ + _createSource (): any { + const onEnd = (err?: Error): void => { + this.close(err) + } + const source = pushableV({ + objectMode: true, + onEnd + }) + + return Object.assign(encode(source, this._init.minSendBytes), { + push: source.push, + end: source.end, + return: source.return + }) + } + + async _handleIncoming (message: Message): Promise { + const { id, type } = message + + if (log.enabled) { + log.trace('incoming message', printMessage(message)) + } + + // Create a new stream? + if (message.type === MessageTypes.NEW_STREAM) { + if (this._streams.receivers.size === (this._init.maxInboundStreams ?? MAX_STREAMS_INBOUND_STREAMS_PER_CONNECTION)) { + log('too many inbound streams open') + + // not going to allow this stream, send the reset message manually + // instead of setting it up just to tear it down + this._source.push({ + id, + type: MessageTypes.RESET_RECEIVER + }) + + // if we've hit our stream limit, and the remote keeps trying to open + // more new streams, if they are doing this very quickly maybe they + // are attacking us and we should close the connection + try { + await this.rateLimiter.consume('new-stream', 1) + } catch { + log('rate limit hit when opening too many new streams over the inbound stream limit - closing remote connection') + // since there's no backpressure in mplex, the only thing we can really do to protect ourselves is close the connection + this._source.end(new Error('Too many open streams')) + return + } + + return + } + + const stream = this._newReceiverStream({ id, name: uint8ArrayToString(message.data instanceof Uint8Array ? message.data : message.data.subarray()) }) + + if (this._init.onIncomingStream != null) { + this._init.onIncomingStream(stream) + } + + return + } + + const list = (type & 1) === 1 ? this._streams.initiators : this._streams.receivers + const stream = list.get(id) + + if (stream == null) { + log('missing stream %s for message type %s', id, MessageTypeNames[type]) + + return + } + + const maxBufferSize = this._init.maxStreamBufferSize ?? MAX_STREAM_BUFFER_SIZE + + switch (type) { + case MessageTypes.MESSAGE_INITIATOR: + case MessageTypes.MESSAGE_RECEIVER: + if (stream.sourceReadableLength() > maxBufferSize) { + // Stream buffer has got too large, reset the stream + this._source.push({ + id: message.id, + type: type === MessageTypes.MESSAGE_INITIATOR ? MessageTypes.RESET_RECEIVER : MessageTypes.RESET_INITIATOR + }) + + // Inform the stream consumer they are not fast enough + const error = new CodeError('Input buffer full - increase Mplex maxBufferSize to accommodate slow consumers', 'ERR_STREAM_INPUT_BUFFER_FULL') + stream.abort(error) + + return + } + + // We got data from the remote, push it into our local stream + stream.sourcePush(message.data) + break + case MessageTypes.CLOSE_INITIATOR: + case MessageTypes.CLOSE_RECEIVER: + // We should expect no more data from the remote, stop reading + stream.closeRead() + break + case MessageTypes.RESET_INITIATOR: + case MessageTypes.RESET_RECEIVER: + // Stop reading and writing to the stream immediately + stream.reset() + break + default: + log('unknown message type %s', type) + } + } +} diff --git a/packages/stream-multiplexer-mplex/src/stream.ts b/packages/stream-multiplexer-mplex/src/stream.ts new file mode 100644 index 0000000000..14d705ecb9 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/stream.ts @@ -0,0 +1,71 @@ +import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { MAX_MSG_SIZE } from './decode.js' +import { InitiatorMessageTypes, ReceiverMessageTypes } from './message-types.js' +import type { Message } from './message-types.js' + +export interface Options { + id: number + send: (msg: Message) => void + name?: string + onEnd?: (err?: Error) => void + type?: 'initiator' | 'receiver' + maxMsgSize?: number +} + +interface MplexStreamInit extends AbstractStreamInit { + streamId: number + name: string + send: (msg: Message) => void +} + +class MplexStream extends AbstractStream { + private readonly name: string + private readonly streamId: number + private readonly send: (msg: Message) => void + private readonly types: Record + + constructor (init: MplexStreamInit) { + super(init) + + this.types = init.direction === 'outbound' ? InitiatorMessageTypes : ReceiverMessageTypes + this.send = init.send + this.name = init.name + this.streamId = init.streamId + } + + sendNewStream (): void { + this.send({ id: this.streamId, type: InitiatorMessageTypes.NEW_STREAM, data: new Uint8ArrayList(uint8ArrayFromString(this.name)) }) + } + + sendData (data: Uint8ArrayList): void { + this.send({ id: this.streamId, type: this.types.MESSAGE, data }) + } + + sendReset (): void { + this.send({ id: this.streamId, type: this.types.RESET }) + } + + sendCloseWrite (): void { + this.send({ id: this.streamId, type: this.types.CLOSE }) + } + + sendCloseRead (): void { + // mplex does not support close read, only close write + } +} + +export function createStream (options: Options): MplexStream { + const { id, name, send, onEnd, type = 'initiator', maxMsgSize = MAX_MSG_SIZE } = options + + return new MplexStream({ + id: type === 'initiator' ? (`i${id}`) : `r${id}`, + streamId: id, + name: `${name == null ? id : name}`, + direction: type === 'initiator' ? 'outbound' : 'inbound', + maxDataSize: maxMsgSize, + onEnd, + send + }) +} diff --git a/packages/stream-multiplexer-mplex/test/coder.spec.ts b/packages/stream-multiplexer-mplex/test/coder.spec.ts new file mode 100644 index 0000000000..af0b6cb225 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/coder.spec.ts @@ -0,0 +1,94 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { expect } from 'aegir/chai' +import all from 'it-all' +import { Uint8ArrayList } from 'uint8arraylist' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { encode } from '../src/encode.js' +import { decode } from './fixtures/decode.js' +import { messageWithBytes } from './fixtures/utils.js' +import type { Message, NewStreamMessage } from '../src/message-types.js' + +describe('coder', () => { + it('should encode header', async () => { + const source: Message[][] = [[{ id: 17, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('17')) }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + const expectedHeader = uint8ArrayFromString('880102', 'base16') + expect(data.slice(0, expectedHeader.length)).to.equalBytes(expectedHeader) + }) + + it('should decode header', async () => { + const source = [uint8ArrayFromString('8801023137', 'base16')] + for await (const msg of decode()(source)) { + expect(messageWithBytes(msg)).to.be.deep.equal({ id: 17, type: 0, data: uint8ArrayFromString('17') }) + } + }) + + it('should encode several msgs into buffer', async () => { + const source: Message[][] = [[ + { id: 17, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('17')) }, + { id: 19, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('19')) }, + { id: 21, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('21')) } + ]] + + const data = uint8ArrayConcat(await all(encode(source))) + + expect(data).to.equalBytes(uint8ArrayFromString('88010231379801023139a801023231', 'base16')) + }) + + it('should encode from Uint8ArrayList', async () => { + const source: NewStreamMessage[][] = [[{ + id: 17, + type: 0, + data: new Uint8ArrayList( + uint8ArrayFromString(Math.random().toString()), + uint8ArrayFromString(Math.random().toString()) + ) + }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + expect(data).to.equalBytes( + uint8ArrayConcat([ + uint8ArrayFromString('8801', 'base16'), + Uint8Array.from([source[0][0].data.length]), + source[0][0].data instanceof Uint8Array ? source[0][0].data : source[0][0].data.slice() + ]) + ) + }) + + it('should decode msgs from buffer', async () => { + const source = [uint8ArrayFromString('88010231379801023139a801023231', 'base16')] + + const res = [] + for await (const msg of decode()(source)) { + res.push(msg) + } + + expect(res.map(messageWithBytes)).to.deep.equal([ + { id: 17, type: 0, data: uint8ArrayFromString('17') }, + { id: 19, type: 0, data: uint8ArrayFromString('19') }, + { id: 21, type: 0, data: uint8ArrayFromString('21') } + ]) + }) + + it('should encode zero length body msg', async () => { + const source: Message[][] = [[{ id: 17, type: 0 }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + expect(data).to.equalBytes(uint8ArrayFromString('880100', 'base16')) + }) + + it('should decode zero length body msg', async () => { + const source = [uint8ArrayFromString('880100', 'base16')] + + for await (const msg of decode()(source)) { + expect(messageWithBytes(msg)).to.be.eql({ id: 17, type: 0, data: new Uint8Array(0) }) + } + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/compliance.spec.ts b/packages/stream-multiplexer-mplex/test/compliance.spec.ts new file mode 100644 index 0000000000..3162211646 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/compliance.spec.ts @@ -0,0 +1,16 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-stream-muxer-compliance-tests' +import { mplex } from '../src/index.js' + +describe('compliance', () => { + tests({ + async setup () { + return mplex({ + maxInboundStreams: Infinity, + disconnectThreshold: Infinity + })() + }, + async teardown () {} + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/fixtures/decode.ts b/packages/stream-multiplexer-mplex/test/fixtures/decode.ts new file mode 100644 index 0000000000..a050d4fd2a --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/fixtures/decode.ts @@ -0,0 +1,19 @@ +/* eslint-env mocha */ + +import { Decoder, MAX_MSG_QUEUE_SIZE, MAX_MSG_SIZE } from '../../src/decode.js' +import type { Message } from '../../src/message-types.js' +import type { Source } from 'it-stream-types' + +export function decode (maxMessageSize: number = MAX_MSG_SIZE, maxUnprocessedMessageQueueSize: number = MAX_MSG_QUEUE_SIZE) { + return async function * decodeMessages (source: Source): Source { + const decoder = new Decoder(maxMessageSize, maxUnprocessedMessageQueueSize) + + for await (const chunk of source) { + const msgs = decoder.write(chunk) + + if (msgs.length > 0) { + yield * msgs + } + } + } +} diff --git a/packages/stream-multiplexer-mplex/test/fixtures/utils.ts b/packages/stream-multiplexer-mplex/test/fixtures/utils.ts new file mode 100644 index 0000000000..edc3d1ed27 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/fixtures/utils.ts @@ -0,0 +1,18 @@ +import { type Message, MessageTypes } from '../../src/message-types.js' + +export type MessageWithBytes = { + [k in keyof Message]: Message[k] +} & { + data: Uint8Array +} + +export function messageWithBytes (msg: Message): Message | MessageWithBytes { + if (msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) { + return { + ...msg, + data: msg.data.slice() // convert Uint8ArrayList to Uint8Array + } + } + + return msg +} diff --git a/packages/stream-multiplexer-mplex/test/mplex.spec.ts b/packages/stream-multiplexer-mplex/test/mplex.spec.ts new file mode 100644 index 0000000000..d6e9a63bb9 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/mplex.spec.ts @@ -0,0 +1,227 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import { pushable } from 'it-pushable' +import pDefer from 'p-defer' +import { Uint8ArrayList } from 'uint8arraylist' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { encode } from '../src/encode.js' +import { mplex } from '../src/index.js' +import { type CloseInitiatorMessage, type Message, type MessageInitiatorMessage, MessageTypes, type NewStreamMessage } from '../src/message-types.js' +import { decode } from './fixtures/decode.js' +import type { Source } from 'it-stream-types' + +describe('mplex', () => { + it('should restrict number of initiator streams per connection', async () => { + const maxOutboundStreams = 10 + const factory = mplex({ + maxOutboundStreams + })() + const muxer = factory.createStreamMuxer() + + // max out the streams for this connection + for (let i = 0; i < maxOutboundStreams; i++) { + await muxer.newStream() + } + + // open one more + await expect((async () => { + await muxer.newStream() + })()).eventually.be.rejected + .with.property('code', 'ERR_TOO_MANY_OUTBOUND_STREAMS') + }) + + it('should restrict number of recipient streams per connection', async () => { + const maxInboundStreams = 10 + const factory = mplex({ + maxInboundStreams, + disconnectThreshold: Infinity + })() + const muxer = factory.createStreamMuxer() + const stream = pushable() + + // max out the streams for this connection + for (let i = 0; i < maxInboundStreams; i++) { + const source: NewStreamMessage[][] = [[{ + id: i, + type: 0, + data: new Uint8ArrayList(uint8ArrayFromString('17')) + }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + stream.push(data) + } + + // simulate a new incoming stream + const source: NewStreamMessage[][] = [[{ + id: 11, + type: 0, + data: new Uint8ArrayList(uint8ArrayFromString('17')) + }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + stream.push(data) + stream.end() + + const bufs: Uint8Array[] = [] + const sinkDone = pDefer() + + void Promise.resolve().then(async () => { + for await (const buf of muxer.source) { + bufs.push(buf) + } + sinkDone.resolve() + }) + + await muxer.sink(stream) + await sinkDone.promise + + const messages = await all(decode()(bufs)) + + expect(messages).to.have.nested.property('[0].id', 11, 'Did not specify the correct stream id') + expect(messages).to.have.nested.property('[0].type', MessageTypes.RESET_RECEIVER, 'Did not reset the stream that tipped us over the inbound stream limit') + }) + + it('should reset a stream that fills the message buffer', async () => { + let sent = 0 + const streamSourceError = pDefer() + const maxStreamBufferSize = 1024 * 1024 // 1MB + const id = 17 + + // simulate a new incoming stream that sends lots of data + const input: Source = (async function * send () { + const newStreamMessage: NewStreamMessage = { + id, + type: MessageTypes.NEW_STREAM, + data: new Uint8ArrayList(new Uint8Array(1024)) + } + yield [newStreamMessage] + + await delay(10) + + for (let i = 0; i < 100; i++) { + const dataMessage: MessageInitiatorMessage = { + id, + type: MessageTypes.MESSAGE_INITIATOR, + data: new Uint8ArrayList(new Uint8Array(1024 * 1000)) + } + yield [dataMessage] + + sent++ + + await delay(10) + } + + await delay(10) + + const closeMessage: CloseInitiatorMessage = { + id, + type: MessageTypes.CLOSE_INITIATOR + } + yield [closeMessage] + })() + + // create the muxer + const factory = mplex({ + maxStreamBufferSize + })() + const muxer = factory.createStreamMuxer({ + onIncomingStream () { + // do nothing with the stream so the buffer fills up + }, + onStreamEnd (stream) { + void all(stream.source) + .then(() => { + streamSourceError.reject(new Error('Stream source did not error')) + }) + .catch(err => { + // should have errored before all 102 messages were sent + expect(sent).to.be.lessThan(10) + streamSourceError.resolve(err) + }) + } + }) + + // collect outgoing mplex messages + const muxerFinished = pDefer() + let messages: Message[] = [] + void Promise.resolve().then(async () => { + messages = await all(decode()(muxer.source)) + muxerFinished.resolve() + }) + + // the muxer processes the messages + await muxer.sink(encode(input)) + + // source should have errored with appropriate code + const err = await streamSourceError.promise + expect(err).to.have.property('code', 'ERR_STREAM_INPUT_BUFFER_FULL') + + // should have sent reset message to peer for this stream + await muxerFinished.promise + expect(messages).to.have.nested.property('[0].id', id) + expect(messages).to.have.nested.property('[0].type', MessageTypes.RESET_RECEIVER) + }) + + it('should batch bytes to send', async () => { + const minSendBytes = 10 + + // input bytes, smaller than batch size + const input: Uint8Array[] = [ + Uint8Array.from([0, 1, 2, 3, 4]), + Uint8Array.from([0, 1, 2, 3, 4]), + Uint8Array.from([0, 1, 2, 3, 4]) + ] + + // create the muxer + const factory = mplex({ + minSendBytes + })() + const muxer = factory.createStreamMuxer({}) + + // collect outgoing mplex messages + const muxerFinished = pDefer() + let output: Uint8Array[] = [] + void Promise.resolve().then(async () => { + output = await all(muxer.source) + muxerFinished.resolve() + }) + + // create a stream + const stream = await muxer.newStream() + const streamFinished = pDefer() + // send messages over the stream + void Promise.resolve().then(async () => { + await stream.sink(async function * () { + yield * input + }()) + stream.close() + streamFinished.resolve() + }) + + // wait for all data to be sent over the stream + await streamFinished.promise + + // close the muxer + await muxer.sink([]) + + // wait for all output to be collected + await muxerFinished.promise + + // last message is unbatched + const closeMessage = output.pop() + expect(closeMessage).to.have.lengthOf(2) + + // all other messages should be above or equal to the batch size + expect(output).to.have.lengthOf(2) + for (const buf of output) { + expect(buf).to.have.length.that.is.at.least(minSendBytes) + } + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/restrict-size.spec.ts b/packages/stream-multiplexer-mplex/test/restrict-size.spec.ts new file mode 100644 index 0000000000..ba23704537 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/restrict-size.spec.ts @@ -0,0 +1,125 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import drain from 'it-drain' +import each from 'it-foreach' +import { pipe } from 'it-pipe' +import toBuffer from 'it-to-buffer' +import { Uint8ArrayList } from 'uint8arraylist' +import { encode } from '../src/encode.js' +import { type Message, MessageTypes } from '../src/message-types.js' +import { decode } from './fixtures/decode.js' + +describe('restrict size', () => { + it('should throw when size is too big', async () => { + const maxSize = 32 + + const input: Message[][] = [ + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(8)) }], + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }], + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(maxSize)) }], + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(64)) }] + ] + + const output: Message[] = [] + + try { + await pipe( + input, + encode, + decode(maxSize), + (source) => each(source, chunk => { + output.push(chunk) + }), + async (source) => { await drain(source) } + ) + } catch (err: any) { + expect(err).to.have.property('code', 'ERR_MSG_TOO_BIG') + expect(output).to.have.length(3) + expect(output[0]).to.deep.equal(input[0][0]) + expect(output[1]).to.deep.equal(input[1][0]) + expect(output[2]).to.deep.equal(input[2][0]) + return + } + throw new Error('did not restrict size') + }) + + it('should allow message with no data property', async () => { + const message: Message = { + id: 4, + type: MessageTypes.CLOSE_RECEIVER + } + const input: Message[][] = [[message]] + + const output = await pipe( + input, + encode, + decode(32), + async (source) => all(source) + ) + expect(output).to.deep.equal(input[0]) + }) + + it('should throw when unprocessed message queue size is too big', async () => { + const maxMessageSize = 32 + const maxUnprocessedMessageQueueSize = 64 + + const input: Message[][] = [[ + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) } + ]] + + const output: Message[] = [] + + try { + await pipe( + input, + encode, + async function * (source) { + // make one big buffer + yield toBuffer(source) + }, + decode(maxMessageSize, maxUnprocessedMessageQueueSize), + (source) => each(source, chunk => { + output.push(chunk) + }), + async (source) => { await drain(source) } + ) + } catch (err: any) { + expect(err).to.have.property('code', 'ERR_MSG_QUEUE_TOO_BIG') + expect(output).to.have.length(0) + return + } + throw new Error('did not restrict size') + }) + + it('should throw when unprocessed message queue size is too big because of garbage', async () => { + const maxMessageSize = 32 + const maxUnprocessedMessageQueueSize = 64 + const input = randomBytes(maxUnprocessedMessageQueueSize + 1) + const output: Message[] = [] + + try { + await pipe( + [input], + decode(maxMessageSize, maxUnprocessedMessageQueueSize), + (source) => each(source, chunk => { + output.push(chunk) + }), + async (source) => { await drain(source) } + ) + } catch (err: any) { + expect(err).to.have.property('code', 'ERR_MSG_QUEUE_TOO_BIG') + expect(output).to.have.length(0) + return + } + throw new Error('did not restrict size') + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/stream.spec.ts b/packages/stream-multiplexer-mplex/test/stream.spec.ts new file mode 100644 index 0000000000..ee6e97e2dd --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/stream.spec.ts @@ -0,0 +1,613 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import * as cborg from 'cborg' +import randomBytes from 'iso-random-stream/src/random.js' +import drain from 'it-drain' +import each from 'it-foreach' +import map from 'it-map' +import { pipe } from 'it-pipe' +import defer from 'p-defer' +import randomInt from 'random-int' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays' +import { MessageTypes, MessageTypeNames } from '../src/message-types.js' +import { createStream } from '../src/stream.js' +import { messageWithBytes } from './fixtures/utils.js' +import type { Message } from '../src/message-types.js' +import type { MplexStream } from '../src/mplex.js' + +function randomInput (min = 1, max = 100): Uint8ArrayList[] { + return Array.from(Array(randomInt(min, max)), () => new Uint8ArrayList(randomBytes(randomInt(1, 128)))) +} + +function expectMsgType (actual: keyof typeof MessageTypeNames, expected: keyof typeof MessageTypeNames): void { + expect(MessageTypeNames[actual]).to.equal(MessageTypeNames[expected]) +} + +function echoedMessage (message: Message): Message { + if (message.type !== MessageTypes.MESSAGE_RECEIVER) { + throw new Error('Message was not a receiver message') + } + + return bufferToMessage(message.data.slice()) +} + +function expectMessages (messages: Message[], codes: Array): void { + messages.slice(0, codes.length).forEach((msg, index) => { + expect(msg).to.have.property('type', codes[index]) + + if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + expect(messageWithBytes(msg)).to.have.property('data').that.equalBytes([index - 1]) + } + }) +} + +function expectEchoedMessages (messages: Message[], codes: Array): void { + expectMessages(messages.slice(0, codes.length).map(echoedMessage), codes) +} + +const msgToBuffer = (msg: Message): Uint8ArrayList => { + const m: any = { + ...msg + } + + if (msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) { + m.data = msg.data.slice() + } + + return new Uint8ArrayList(cborg.encode(m)) +} + +const bufferToMessage = (buf: Uint8Array | Uint8ArrayList): Message => cborg.decode(buf.subarray()) + +interface onMessage { + (msg: Message, initator: MplexStream, receiver: MplexStream): void +} + +export interface StreamPair { + initiatorMessages: Message[] + receiverMessages: Message[] +} + +async function streamPair (n: number, onInitiatorMessage?: onMessage, onReceiverMessage?: onMessage): Promise { + const receiverMessages: Message[] = [] + const initiatorMessages: Message[] = [] + const id = 5 + + const mockInitiatorSend = (msg: Message): void => { + initiatorMessages.push(msg) + + if (onInitiatorMessage != null) { + onInitiatorMessage(msg, initiator, receiver) + } + + receiver.sourcePush(msgToBuffer(msg)) + } + const mockReceiverSend = (msg: Message): void => { + receiverMessages.push(msg) + + if (onReceiverMessage != null) { + onReceiverMessage(msg, initiator, receiver) + } + + initiator.sourcePush(msgToBuffer(msg)) + } + const initiator = createStream({ id, send: mockInitiatorSend, type: 'initiator' }) + const receiver = createStream({ id, send: mockReceiverSend, type: 'receiver' }) + const input = new Array(n).fill(0).map((_, i) => new Uint8ArrayList(Uint8Array.from([i]))) + + void pipe( + receiver, + source => each(source, buf => { + const msg = bufferToMessage(buf) + + // when the initiator sends a CLOSE message, we call close + if (msg.type === MessageTypes.CLOSE_INITIATOR) { + receiver.closeRead() + } + + // when the initiator sends a RESET message, we call close + if (msg.type === MessageTypes.RESET_INITIATOR) { + receiver.reset() + } + }), + receiver + ).catch(() => {}) + + try { + await pipe( + input, + initiator, + (source) => map(source, buf => { + const msg: Message = bufferToMessage(buf) + + // when the receiver sends a CLOSE message, we call close + if (msg.type === MessageTypes.CLOSE_RECEIVER) { + initiator.close() + } + + // when the receiver sends a RESET message, we call close + if (msg.type === MessageTypes.RESET_RECEIVER) { + initiator.reset() + } + }), + drain + ) + } catch { + + } + + return { + receiverMessages, + initiatorMessages + } +} + +describe('stream', () => { + it('should initiate stream with NEW_STREAM message', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const stream = createStream({ id, send: mockSend }) + const input = randomInput() + + await pipe(input, stream) + + expect(msgs[0].id).to.equal(id) + expectMsgType(msgs[0].type, MessageTypes.NEW_STREAM) + expect(messageWithBytes(msgs[0])).to.have.property('data').that.equalBytes(uint8ArrayFromString(id.toString())) + }) + + it('should initiate named stream with NEW_STREAM message', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const stream = createStream({ id, name, send: mockSend }) + const input = randomInput() + + await pipe(input, stream) + + expect(msgs[0].id).to.equal(id) + expectMsgType(msgs[0].type, MessageTypes.NEW_STREAM) + expect(messageWithBytes(msgs[0])).to.have.property('data').that.equalBytes(uint8ArrayFromString(name)) + }) + + it('should end a stream when it is aborted', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const deferred = defer() + const stream = createStream({ id, name, onEnd: deferred.resolve, send: mockSend }) + + const error = new Error('boom') + stream.abort(error) + + const err = await deferred.promise + expect(err).to.equal(error) + }) + + it('should end a stream when it is reset', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const deferred = defer() + const stream = createStream({ id, name, onEnd: deferred.resolve, send: mockSend }) + + stream.reset() + + const err = await deferred.promise + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_STREAM_RESET') + }) + + it('should send data with MESSAGE_INITIATOR messages if stream initiator', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'initiator' }) + const input = randomInput() + + await pipe(input, stream) + + // First and last should be NEW_STREAM and CLOSE + const dataMsgs = msgs.slice(1, -1) + expect(dataMsgs).have.length(input.length) + + dataMsgs.forEach((msg, i) => { + expect(msg.id).to.equal(id) + expectMsgType(msg.type, MessageTypes.MESSAGE_INITIATOR) + expect(messageWithBytes(msg)).to.have.property('data').that.equalBytes(input[i].subarray()) + }) + }) + + it('should send data with MESSAGE_RECEIVER messages if stream receiver', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'receiver' }) + const input = randomInput() + + await pipe(input, stream) + + // Last should be CLOSE + const dataMsgs = msgs.slice(0, -1) + expect(dataMsgs).have.length(input.length) + + dataMsgs.forEach((msg, i) => { + expect(msg.id).to.equal(id) + expectMsgType(msg.type, MessageTypes.MESSAGE_RECEIVER) + expect(messageWithBytes(msg)).to.have.property('data').that.equalBytes(input[i].subarray()) + }) + }) + + it('should close stream with CLOSE_INITIATOR message if stream initiator', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'initiator' }) + const input = randomInput() + + await pipe(input, stream) + + const closeMsg = msgs[msgs.length - 1] + + expect(closeMsg.id).to.equal(id) + expectMsgType(closeMsg.type, MessageTypes.CLOSE_INITIATOR) + expect(closeMsg).to.not.have.property('data') + }) + + it('should close stream with CLOSE_RECEIVER message if stream receiver', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'receiver' }) + const input = randomInput() + + await pipe(input, stream) + + const closeMsg = msgs[msgs.length - 1] + + expect(closeMsg.id).to.equal(id) + expectMsgType(closeMsg.type, MessageTypes.CLOSE_RECEIVER) + expect(closeMsg).to.not.have.property('data') + }) + + it('should reset stream on error with RESET_INITIATOR message if stream initiator', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'initiator' }) + const error = new Error(`Boom ${Date.now()}`) + const input = { + [Symbol.iterator]: function * () { + for (let i = 0; i < randomInt(1, 10); i++) { + yield new Uint8ArrayList(randomBytes(randomInt(1, 128))) + } + throw error + } + } + + await expect(pipe(input, stream)).to.eventually.be + .rejected.with.property('message', error.message) + + const resetMsg = msgs[msgs.length - 1] + + expect(resetMsg.id).to.equal(id) + expectMsgType(resetMsg.type, MessageTypes.RESET_INITIATOR) + expect(resetMsg).to.not.have.property('data') + }) + + it('should reset stream on error with RESET_RECEIVER message if stream receiver', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'receiver' }) + const error = new Error(`Boom ${Date.now()}`) + const input = { + [Symbol.iterator]: function * () { + for (let i = 0; i < randomInt(1, 10); i++) { + yield new Uint8ArrayList(randomBytes(randomInt(1, 128))) + } + throw error + } + } + + await expect(pipe(input, stream)).to.eventually.be.rejected + .with.property('message', error.message) + + const resetMsg = msgs[msgs.length - 1] + + expect(resetMsg.id).to.equal(id) + expectMsgType(resetMsg.type, MessageTypes.RESET_RECEIVER) + expect(resetMsg).to.not.have.property('data') + }) + + it('should close for reading (remote close)', async () => { + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength) + + // 1x NEW_STREAM, dataLength x MESSAGE_INITIATOR 1x CLOSE_INITIATOR + expect(initiatorMessages).to.have.lengthOf(1 + dataLength + 1) + expectMessages(initiatorMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.CLOSE_INITIATOR + ]) + + // all the initiator messages plus CLOSE_RECEIVER + expect(receiverMessages).to.have.lengthOf(8) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.CLOSE_INITIATOR + ]) + expect(receiverMessages[receiverMessages.length - 1]).to.have.property('type', MessageTypes.CLOSE_RECEIVER) + }) + + it('should close for reading and writing (abort on local error)', async () => { + const maxMsgs = 2 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, (initiatorMessage, initiator) => { + messages++ + + if (messages === maxMsgs) { + initiator.abort(error) + } + }) + + expect(initiatorMessages).to.have.lengthOf(3) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.RESET_INITIATOR) + + // Reset after two messages + expect(receiverMessages).to.have.lengthOf(2) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR + ]) + }) + + it('should close for reading and writing (abort on remote error)', async () => { + const maxMsgs = 4 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, (initiatorMessage, initiator, recipient) => { + messages++ + + if (messages === maxMsgs) { + recipient.abort(error) + } + }) + + // All messages sent to recipient + expect(initiatorMessages).to.have.lengthOf(7) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[3]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[4]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[5]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[6]).to.have.property('type', MessageTypes.CLOSE_INITIATOR) + + // Recipient reset after two messages + expect(receiverMessages).to.have.lengthOf(3) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR + ]) + expect(receiverMessages[receiverMessages.length - 1]).to.have.property('type', MessageTypes.RESET_RECEIVER) + }) + + it('should close immediately for reading and writing (reset on local error)', async () => { + const maxMsgs = 2 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, () => { + messages++ + + if (messages === maxMsgs) { + throw error + } + }) + + expect(initiatorMessages).to.have.lengthOf(3) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.RESET_INITIATOR) + + // Reset after two messages + expect(receiverMessages).to.have.lengthOf(1) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM + ]) + }) + + it('should close immediately for reading and writing (reset on remote error)', async () => { + const maxMsgs = 2 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, () => {}, () => { + messages++ + + if (messages === maxMsgs) { + throw error + } + }) + + // All messages sent to recipient + expect(initiatorMessages).to.have.lengthOf(7) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[3]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[4]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[5]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[6]).to.have.property('type', MessageTypes.CLOSE_INITIATOR) + + // Recipient reset after two messages + expect(receiverMessages).to.have.lengthOf(3) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR + ]) + expect(receiverMessages[receiverMessages.length - 1]).to.have.property('type', MessageTypes.RESET_RECEIVER) + }) + + it('should call onEnd only when both sides have closed', async () => { + const send = (msg: Message): void => { + if (msg.type === MessageTypes.CLOSE_INITIATOR) { + // simulate remote closing connection + stream.closeRead() + } else if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + stream.sourcePush(msgToBuffer(msg)) + } + } + const id = randomInt(1000) + const name = id.toString() + const deferred = defer() + const onEnd = (err?: any): void => { err != null ? deferred.reject(err) : deferred.resolve() } + const stream = createStream({ id, name, send, onEnd }) + const input = randomInput() + + void pipe( + input, + stream, + drain + ) + + await deferred.promise + }) + + it('should call onEnd with error for local error', async () => { + const send = (): void => { + throw new Error(`Local boom ${Date.now()}`) + } + const id = randomInt(1000) + const deferred = defer() + const onEnd = (err?: any): void => { err != null ? deferred.reject(err) : deferred.resolve() } + const stream = createStream({ id, send, onEnd }) + const input = randomInput() + + pipe( + input, + stream, + drain + ).catch(() => {}) + + await expect(deferred.promise).to.eventually.be.rejectedWith(/Local boom/) + }) + + it('should split writes larger than max message size', async () => { + const messages: Message[] = [] + + const send = (msg: Message): void => { + if (msg.type === MessageTypes.CLOSE_INITIATOR) { + stream.closeRead() + } else if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + messages.push(msg) + } + } + const maxMsgSize = 10 + const id = randomInt(1000) + const stream = createStream({ id, send, maxMsgSize }) + + await pipe( + [ + new Uint8ArrayList(new Uint8Array(maxMsgSize * 2)) + ], + stream, + drain + ) + + expect(messages.length).to.equal(2) + expect(messages[0]).to.have.nested.property('data.length', maxMsgSize) + expect(messages[1]).to.have.nested.property('data.length', maxMsgSize) + }) + + it('should error on double-sink', async () => { + const send = (): void => {} + const id = randomInt(1000) + const stream = createStream({ id, send }) + + // first sink is ok + await stream.sink([]) + + // cannot sink twice + await expect(stream.sink([])) + .to.eventually.be.rejected.with.property('code', 'ERR_DOUBLE_SINK') + }) + + it('should chunk really big messages', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const maxMsgSize = 10 + const stream = createStream({ id, name, send: mockSend, maxMsgSize }) + const input = [ + new Uint8Array(1024).map(() => randomInt(0, 255)) + ] + const output = new Uint8ArrayList() + + await pipe(input, stream) + + expect(msgs).to.have.lengthOf(105) + expect(msgs[0].id).to.equal(id) + expectMsgType(msgs[0].type, MessageTypes.NEW_STREAM) + + for (let i = 1; i < msgs.length - 1; i++) { + const msg = msgs[i] + expectMsgType(msg.type, MessageTypes.MESSAGE_INITIATOR) + + if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + output.append(msg.data) + } + } + + expectMsgType(msgs[msgs.length - 1].type, MessageTypes.CLOSE_INITIATOR) + expect(output.subarray()).to.equalBytes(input[0]) + }) +}) diff --git a/packages/stream-multiplexer-mplex/tsconfig.json b/packages/stream-multiplexer-mplex/tsconfig.json new file mode 100644 index 0000000000..5eeae73ed9 --- /dev/null +++ b/packages/stream-multiplexer-mplex/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-stream-muxer" + }, + { + "path": "../interface-stream-muxer-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + } + ] +} diff --git a/packages/topology/CHANGELOG.md b/packages/topology/CHANGELOG.md new file mode 100644 index 0000000000..bad4141262 --- /dev/null +++ b/packages/topology/CHANGELOG.md @@ -0,0 +1,248 @@ +## [4.0.3](https://github.com/libp2p/js-libp2p-topology/compare/v4.0.2...v4.0.3) (2023-06-15) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.8 ([#31](https://github.com/libp2p/js-libp2p-topology/issues/31)) ([628f1a8](https://github.com/libp2p/js-libp2p-topology/commit/628f1a8618411dff87c9283292da0e43f95d57d1)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-topology/compare/v4.0.1...v4.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([944bcae](https://github.com/libp2p/js-libp2p-topology/commit/944bcae65d709ac70f986df403e367cb898f7700)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([8d3e9af](https://github.com/libp2p/js-libp2p-topology/commit/8d3e9afa5925856b5ad89e46d87aa944ff7678d3)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([aae4c14](https://github.com/libp2p/js-libp2p-topology/commit/aae4c146b0fa310920a229867ac188a89cb5b951)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([eed0e46](https://github.com/libp2p/js-libp2p-topology/commit/eed0e462b9c9d9dd51adffd9755f1bb7fa911432)) +* Update .github/workflows/stale.yml [skip ci] ([fdc316d](https://github.com/libp2p/js-libp2p-topology/commit/fdc316d09807aeb69df9c63810255a249c311bad)) + + +### Dependencies + +* bump it-all from 2.0.1 to 3.0.1 ([#32](https://github.com/libp2p/js-libp2p-topology/issues/32)) ([d2db182](https://github.com/libp2p/js-libp2p-topology/commit/d2db18262c918db5a61a946a3b42a406031d4861)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-topology/compare/v4.0.0...v4.0.1) (2023-01-13) + + +### Dependencies + +* remove err-code ([#18](https://github.com/libp2p/js-libp2p-topology/issues/18)) ([70cc52b](https://github.com/libp2p/js-libp2p-topology/commit/70cc52b0da883fba9288fab1de362d3f71707f28)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-topology/compare/v3.0.2...v4.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update to multiformats v11 (#17) + +### Bug Fixes + +* update to multiformats v11 ([#17](https://github.com/libp2p/js-libp2p-topology/issues/17)) ([e7d9c91](https://github.com/libp2p/js-libp2p-topology/commit/e7d9c91e1c0697ec885828c661e874dc15bd0919)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-topology/compare/v3.0.1...v3.0.2) (2022-12-16) + + +### Documentation + +* publish api docs ([#16](https://github.com/libp2p/js-libp2p-topology/issues/16)) ([285caba](https://github.com/libp2p/js-libp2p-topology/commit/285caba282d75cb9156a298c7a1b6463d58e3064)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-topology/compare/v3.0.0...v3.0.1) (2022-09-21) + + +### Bug Fixes + +* remove unused deps and update project config ([#9](https://github.com/libp2p/js-libp2p-topology/issues/9)) ([2aa138f](https://github.com/libp2p/js-libp2p-topology/commit/2aa138f4784662ada8f82304d2b50858a0b0e5ba)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([ce9f715](https://github.com/libp2p/js-libp2p-topology/commit/ce9f71582680514156031f75fa00e8925d12b85e)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-pubsub-topology/compare/v2.0.0...v3.0.0) (2022-06-16) + + +### ⚠ BREAKING CHANGES + +* registrar API has changed + +### Trivial Changes + +* update deps ([#6](https://github.com/libp2p/js-libp2p-pubsub-topology/issues/6)) ([ad2330b](https://github.com/libp2p/js-libp2p-pubsub-topology/commit/ad2330be33f4bf5ba91ef67f92eb23109504fb0a)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-pubsub-topology/compare/v1.1.9...v2.0.0) (2022-06-14) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#2](https://github.com/libp2p/js-libp2p-pubsub-topology/issues/2)) ([201ade5](https://github.com/libp2p/js-libp2p-pubsub-topology/commit/201ade5b8ce2b8233d267f71a5ffd685110a4115)) + +### [1.1.9](https://github.com/libp2p/js-libp2p-pubsub-topology/compare/v1.1.8...v1.1.9) (2022-06-09) + + +### Trivial Changes + +* update readme ([#1](https://github.com/libp2p/js-libp2p-pubsub-topology/issues/1)) ([f1cfdc6](https://github.com/libp2p/js-libp2p-pubsub-topology/commit/f1cfdc67808ae2ec6fa01a54f96d708129b25371)) + +## [@libp2p/topology-v1.1.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.7...@libp2p/topology-v1.1.8) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/topology-v1.1.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.6...@libp2p/topology-v1.1.7) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/topology-v1.1.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.5...@libp2p/topology-v1.1.6) (2022-03-15) + + +### 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)) + +## [@libp2p/topology-v1.1.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.4...@libp2p/topology-v1.1.5) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/topology-v1.1.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.3...@libp2p/topology-v1.1.4) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/topology-v1.1.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.2...@libp2p/topology-v1.1.3) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/topology-v1.1.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.1...@libp2p/topology-v1.1.2) (2022-02-10) + + +### Bug Fixes + +* make registrar simpler ([#163](https://github.com/libp2p/js-libp2p-interfaces/issues/163)) ([d122f3d](https://github.com/libp2p/js-libp2p-interfaces/commit/d122f3daaccc04039d90814960da92b513265644)) + +## [@libp2p/topology-v1.1.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.0...@libp2p/topology-v1.1.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/topology-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.0.3...@libp2p/topology-v1.1.0) (2022-02-09) + + +### 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)) + +## [@libp2p/topology-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.0.2...@libp2p/topology-v1.0.3) (2022-01-15) + + +### Trivial Changes + +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/topology-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.0.1...@libp2p/topology-v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) +* update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.2.0 (2022-01-04) + + +### chore + +* update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) + + +### Features + +* add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) +* split out code, convert to typescript ([#111](https://github.com/libp2p/js-libp2p-interfaces/issues/111)) ([e174bba](https://github.com/libp2p/js-libp2p-interfaces/commit/e174bba889388269b806643c79a6b53c8d6a0f8c)), closes [#110](https://github.com/libp2p/js-libp2p-interfaces/issues/110) [#101](https://github.com/libp2p/js-libp2p-interfaces/issues/101) +* update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) + + +### BREAKING CHANGES + +* requires node 15+ +* not all fields from concrete classes have been added to the interfaces, some adjustment may be necessary as this gets rolled out + + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-topology@0.3.0...libp2p-topology@0.3.1) (2022-01-02) + +**Note:** Version bump only for package libp2p-topology + + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-topology@0.2.0...libp2p-topology@0.3.0) (2022-01-02) + + +### Features + +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) + + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-topology@0.1.0...libp2p-topology@0.2.0) (2021-12-02) + + +### chore + +* update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) + + +### BREAKING CHANGES + +* requires node 15+ + + + + + +# 0.1.0 (2021-11-22) + + +### Features + +* split out code, convert to typescript ([#111](https://github.com/libp2p/js-libp2p-interfaces/issues/111)) ([e174bba](https://github.com/libp2p/js-libp2p-interfaces/commit/e174bba889388269b806643c79a6b53c8d6a0f8c)), closes [#110](https://github.com/libp2p/js-libp2p-interfaces/issues/110) [#101](https://github.com/libp2p/js-libp2p-interfaces/issues/101) + + +### BREAKING CHANGES + +* not all fields from concrete classes have been added to the interfaces, some adjustment may be necessary as this gets rolled out diff --git a/packages/topology/LICENSE b/packages/topology/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/topology/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/topology/LICENSE-APACHE b/packages/topology/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/topology/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/topology/LICENSE-MIT b/packages/topology/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/topology/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/topology/README.md b/packages/topology/README.md new file mode 100644 index 0000000000..915f0bc473 --- /dev/null +++ b/packages/topology/README.md @@ -0,0 +1,45 @@ +# @libp2p/topology + +[![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) + +> libp2p network topology + +## Table of contents + +- [Install](#install) +- [Usage](#usage) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/topology +``` + +## Usage + +```javascript +import { createTopology } from '@libp2p/topology' + +const topology = createTopology({ ... }) +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/topology/package.json b/packages/topology/package.json new file mode 100644 index 0000000000..055834a6bd --- /dev/null +++ b/packages/topology/package.json @@ -0,0 +1,74 @@ +{ + "name": "@libp2p/topology", + "version": "4.0.3", + "description": "libp2p network topology", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/topology#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "interface", + "libp2p" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./multicodec-topology": { + "types": "./dist/src/multicodec-topology.d.ts", + "import": "./dist/src/multicodec-topology.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-registrar": "^2.0.0" + }, + "devDependencies": { + "aegir": "^39.0.10" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/topology/src/index.ts b/packages/topology/src/index.ts new file mode 100644 index 0000000000..d81878b728 --- /dev/null +++ b/packages/topology/src/index.ts @@ -0,0 +1,49 @@ +import { topologySymbol as symbol } from '@libp2p/interface-registrar' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Topology, TopologyInit, onConnectHandler, onDisconnectHandler, Registrar } from '@libp2p/interface-registrar' + +const noop = (): void => {} + +class TopologyImpl implements Topology { + public min: number + public max: number + + /** + * Set of peers that support the protocol + */ + public peers: Set + public onConnect: onConnectHandler + public onDisconnect: onDisconnectHandler + + protected registrar: Registrar | undefined + + constructor (init: TopologyInit) { + this.min = init.min ?? 0 + this.max = init.max ?? Infinity + this.peers = new Set() + + this.onConnect = init.onConnect ?? noop + this.onDisconnect = init.onDisconnect ?? noop + } + + get [Symbol.toStringTag] (): string { + return symbol.toString() + } + + readonly [symbol] = true + + async setRegistrar (registrar: Registrar): Promise { + this.registrar = registrar + } + + /** + * Notify about peer disconnected event + */ + disconnect (peerId: PeerId): void { + this.onDisconnect(peerId) + } +} + +export function createTopology (init: TopologyInit): Topology { + return new TopologyImpl(init) +} diff --git a/packages/topology/tsconfig.json b/packages/topology/tsconfig.json new file mode 100644 index 0000000000..c1017faac2 --- /dev/null +++ b/packages/topology/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-registrar" + } + ] +} diff --git a/packages/tracked-map/.gitignore b/packages/tracked-map/.gitignore new file mode 100644 index 0000000000..1531bdf9de --- /dev/null +++ b/packages/tracked-map/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.docs +.coverage +package-lock.json +yarn.lock diff --git a/packages/tracked-map/CHANGELOG.md b/packages/tracked-map/CHANGELOG.md new file mode 100644 index 0000000000..4d32eb15c7 --- /dev/null +++ b/packages/tracked-map/CHANGELOG.md @@ -0,0 +1,139 @@ +## [3.0.3](https://github.com/libp2p/js-libp2p-tracked-map/compare/v3.0.2...v3.0.3) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6447152](https://github.com/libp2p/js-libp2p-tracked-map/commit/6447152525c274482d6f8e78ef857e68b57d96bd)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([80163d7](https://github.com/libp2p/js-libp2p-tracked-map/commit/80163d7b950106a653441171f7fc455980935f12)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([5fc1905](https://github.com/libp2p/js-libp2p-tracked-map/commit/5fc1905f868da61cdc6f75d5ef108653eb20072e)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([cb4e22d](https://github.com/libp2p/js-libp2p-tracked-map/commit/cb4e22de6b601c52fbab09e384173590e2ecc0d8)) +* Update .github/workflows/stale.yml [skip ci] ([bda0a65](https://github.com/libp2p/js-libp2p-tracked-map/commit/bda0a656569ae5a6e47cf2d58fa83916d4b2af19)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#30](https://github.com/libp2p/js-libp2p-tracked-map/issues/30)) ([42a4d8b](https://github.com/libp2p/js-libp2p-tracked-map/commit/42a4d8be71ea05c435895cf3f139d3fbfee95708)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-tracked-map/compare/v3.0.1...v3.0.2) (2022-12-16) + + +### Documentation + +* update docs ([6ad441d](https://github.com/libp2p/js-libp2p-tracked-map/commit/6ad441df9be1a516a17d2c175218f11af204e559)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-tracked-map/compare/v3.0.0...v3.0.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#20](https://github.com/libp2p/js-libp2p-tracked-map/issues/20)) ([61446e1](https://github.com/libp2p/js-libp2p-tracked-map/commit/61446e15a05988ab3e18eafeac0adf9cb153414f)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-tracked-map/compare/v2.0.2...v3.0.0) (2022-11-05) + + +### ⚠ BREAKING CHANGES + +* requires @libp2p/interface-metrics v4 + +### Bug Fixes + +* update @libp2p/interface-metrics to v4 ([#12](https://github.com/libp2p/js-libp2p-tracked-map/issues/12)) ([6a97551](https://github.com/libp2p/js-libp2p-tracked-map/commit/6a97551c3c3c718c63f7ba3102b83b7ac1e08d8f)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([fb388de](https://github.com/libp2p/js-libp2p-tracked-map/commit/fb388de359071a8c66ae8e17cca14f06a878aa20)) +* update project config ([#13](https://github.com/libp2p/js-libp2p-tracked-map/issues/13)) ([e58d7df](https://github.com/libp2p/js-libp2p-tracked-map/commit/e58d7dfefe80daebee042b4a5925f847ae6ded09)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-tracked-map/compare/v2.0.1...v2.0.2) (2022-08-12) + + +### Dependencies + +* update interface deps ([#9](https://github.com/libp2p/js-libp2p-tracked-map/issues/9)) ([424b9e8](https://github.com/libp2p/js-libp2p-tracked-map/commit/424b9e81db1d1005fcfdfd599f590e7ff1c30a68)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-tracked-map/compare/v2.0.0...v2.0.1) (2022-07-01) + + +### Trivial Changes + +* update @libp2p/interface-metrics ([#7](https://github.com/libp2p/js-libp2p-tracked-map/issues/7)) ([de16bc7](https://github.com/libp2p/js-libp2p-tracked-map/commit/de16bc7776b52b6eee8e5d4a14aa9cff484a21ae)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-tracked-map/compare/v1.0.8...v2.0.0) (2022-06-28) + + +### ⚠ BREAKING CHANGES + +* accepts a @libp2p/interface-metrics ComponentMetricsTracker + +### Bug Fixes + +* update deps ([#5](https://github.com/libp2p/js-libp2p-tracked-map/issues/5)) ([a134743](https://github.com/libp2p/js-libp2p-tracked-map/commit/a1347439d95346f5361ec030e159d53e22402914)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-tracked-map/compare/v1.0.7...v1.0.8) (2022-06-09) + + +### Trivial Changes + +* fix project config ([#1](https://github.com/libp2p/js-libp2p-tracked-map/issues/1)) ([eb3d2ea](https://github.com/libp2p/js-libp2p-tracked-map/commit/eb3d2eaf094d51b4724d040325ac4e7bb3daa74b)) + +## [@libp2p/tracked-map-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.6...@libp2p/tracked-map-v1.0.7) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/tracked-map-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.5...@libp2p/tracked-map-v1.0.6) (2022-05-10) + + +### Trivial Changes + +* **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) + +## [@libp2p/tracked-map-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.4...@libp2p/tracked-map-v1.0.5) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/tracked-map-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.3...@libp2p/tracked-map-v1.0.4) (2022-03-15) + + +### 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)) + +## [@libp2p/tracked-map-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.2...@libp2p/tracked-map-v1.0.3) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/tracked-map-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.1...@libp2p/tracked-map-v1.0.2) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/tracked-map-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.0...@libp2p/tracked-map-v1.0.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## @libp2p/tracked-map-v1.0.0 (2022-02-05) + + +### Features + +* 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/tracked-map/LICENSE b/packages/tracked-map/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/tracked-map/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/tracked-map/LICENSE-APACHE b/packages/tracked-map/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/tracked-map/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/tracked-map/LICENSE-MIT b/packages/tracked-map/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/tracked-map/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/tracked-map/README.md b/packages/tracked-map/README.md new file mode 100644 index 0000000000..0766685453 --- /dev/null +++ b/packages/tracked-map/README.md @@ -0,0 +1,63 @@ +# @libp2p/tracked-map + +[![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) + +> Allows tracking of statistics while libp2p is running + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +A map that reports it's size to the libp2p [Metrics](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/metrics#readme) system. + +If metrics are disabled a regular map is used. + +## Example + +```JavaScript +import { trackedMap } from '@libp2p/tracked-map' + +const map = trackedMap({ metrics }) + +map.set('key', 'value') +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/tracked-map/package.json b/packages/tracked-map/package.json new file mode 100644 index 0000000000..427c7da0de --- /dev/null +++ b/packages/tracked-map/package.json @@ -0,0 +1,62 @@ +{ + "name": "@libp2p/tracked-map", + "version": "3.0.3", + "description": "Allows tracking of statistics while libp2p is running", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/tracked-map#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-metrics": "^4.0.0" + }, + "devDependencies": { + "@types/sinon": "^10.0.15", + "aegir": "^39.0.10", + "sinon": "^15.1.0", + "sinon-ts": "^1.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/tracked-map/src/index.ts b/packages/tracked-map/src/index.ts new file mode 100644 index 0000000000..6cf3fa322c --- /dev/null +++ b/packages/tracked-map/src/index.ts @@ -0,0 +1,65 @@ +import type { Metric, Metrics } from '@libp2p/interface-metrics' + +export interface TrackedMapInit { + name: string + metrics: Metrics +} + +class TrackedMap extends Map { + private readonly metric: Metric + + constructor (init: TrackedMapInit) { + super() + + const { name, metrics } = init + + this.metric = metrics.registerMetric(name) + this.updateComponentMetric() + } + + set (key: K, value: V): this { + super.set(key, value) + this.updateComponentMetric() + return this + } + + delete (key: K): boolean { + const deleted = super.delete(key) + this.updateComponentMetric() + return deleted + } + + clear (): void { + super.clear() + this.updateComponentMetric() + } + + private updateComponentMetric (): void { + this.metric.update(this.size) + } +} + +export interface CreateTrackedMapInit { + /** + * The metric name to use + */ + name: string + + /** + * A metrics implementation + */ + metrics?: Metrics +} + +export function trackedMap (config: CreateTrackedMapInit): Map { + const { name, metrics } = config + let map: Map + + if (metrics != null) { + map = new TrackedMap({ name, metrics }) + } else { + map = new Map() + } + + return map +} diff --git a/packages/tracked-map/test/index.spec.ts b/packages/tracked-map/test/index.spec.ts new file mode 100644 index 0000000000..4450f41df8 --- /dev/null +++ b/packages/tracked-map/test/index.spec.ts @@ -0,0 +1,93 @@ +import { expect } from 'aegir/chai' +import { stubInterface } from 'sinon-ts' +import { trackedMap } from '../src/index.js' +import type { Metric, Metrics } from '@libp2p/interface-metrics' +import type { SinonStubbedInstance } from 'sinon' + +describe('tracked-map', () => { + let metrics: SinonStubbedInstance + + beforeEach(() => { + metrics = stubInterface() + }) + + it('should return a map with metrics', () => { + const name = 'system_component_metric' + const metric = stubInterface() + // @ts-expect-error the wrong overload is selected + metrics.registerMetric.withArgs(name).returns(metric) + + const map = trackedMap({ + name, + metrics + }) + + expect(map).to.be.an.instanceOf(Map) + expect(metrics.registerMetric.calledWith(name)).to.be.true() + }) + + it('should return a map without metrics', () => { + const name = 'system_component_metric' + const metric = stubInterface() + // @ts-expect-error the wrong overload is selected + metrics.registerMetric.withArgs(name).returns(metric) + + const map = trackedMap({ + name + }) + + expect(map).to.be.an.instanceOf(Map) + expect(metrics.registerMetric.called).to.be.false() + }) + + it('should track metrics', () => { + const name = 'system_component_metric' + let value = 0 + let callCount = 0 + + const metric = stubInterface() + // @ts-expect-error the wrong overload is selected + metrics.registerMetric.withArgs(name).returns(metric) + + metric.update.callsFake((v) => { + if (typeof v === 'number') { + value = v + } + + callCount++ + }) + + const map = trackedMap({ + name, + metrics + }) + + expect(map).to.be.an.instanceOf(Map) + expect(callCount).to.equal(1) + + map.set('key1', 'value1') + + expect(value).to.equal(1) + expect(callCount).to.equal(2) + + map.set('key1', 'value2') + + expect(value).to.equal(1) + expect(callCount).to.equal(3) + + map.set('key2', 'value3') + + expect(value).to.equal(2) + expect(callCount).to.equal(4) + + map.delete('key2') + + expect(value).to.equal(1) + expect(callCount).to.equal(5) + + map.clear() + + expect(value).to.equal(0) + expect(callCount).to.equal(6) + }) +}) diff --git a/packages/tracked-map/tsconfig.json b/packages/tracked-map/tsconfig.json new file mode 100644 index 0000000000..c60dfc7151 --- /dev/null +++ b/packages/tracked-map/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-metrics" + } + ] +} diff --git a/packages/transport-tcp/.aegir.js b/packages/transport-tcp/.aegir.js new file mode 100644 index 0000000000..35647b8d49 --- /dev/null +++ b/packages/transport-tcp/.aegir.js @@ -0,0 +1,9 @@ + +export default { + build: { + config: { + platform: 'node' + }, + bundlesizeMax: '31KB' + } +} diff --git a/packages/transport-tcp/CHANGELOG.md b/packages/transport-tcp/CHANGELOG.md new file mode 100644 index 0000000000..67385fe0a7 --- /dev/null +++ b/packages/transport-tcp/CHANGELOG.md @@ -0,0 +1,908 @@ +## [7.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v7.0.2...v7.0.3) (2023-06-15) + + +### Dependencies + +* **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)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v7.0.1...v7.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6dcfdb3](https://github.com/libp2p/js-libp2p-tcp/commit/6dcfdb304010f7002670549e51834e01559a9cfc)) +* Update .github/workflows/stale.yml [skip ci] ([862af6c](https://github.com/libp2p/js-libp2p-tcp/commit/862af6cc96f2c2bac9821db3af6035e601746913)) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 11.0.3 to 12.0.1 ([#274](https://github.com/libp2p/js-libp2p-tcp/issues/274)) ([147610f](https://github.com/libp2p/js-libp2p-tcp/commit/147610f8053dc504eaccf899ae0099eca622a263)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v7.0.0...v7.0.1) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-transport from 3.0.0 to 4.0.0 ([#269](https://github.com/libp2p/js-libp2p-tcp/issues/269)) ([1b06059](https://github.com/libp2p/js-libp2p-tcp/commit/1b06059b4dc5164367ebde1194434426ee4893d1)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v6.2.2...v7.0.0) (2023-04-18) + + +### ⚠ BREAKING CHANGES + +* update stream types (#266) + +### Dependencies + +* update stream types ([#266](https://github.com/libp2p/js-libp2p-tcp/issues/266)) ([d2b69b6](https://github.com/libp2p/js-libp2p-tcp/commit/d2b69b6b08b40e76e0bda1796ebe641cf2ca4c6e)) + +## [6.2.2](https://github.com/libp2p/js-libp2p-tcp/compare/v6.2.1...v6.2.2) (2023-04-17) + + +### Dependencies + +* **dev:** bump it-pipe from 2.0.5 to 3.0.1 ([#261](https://github.com/libp2p/js-libp2p-tcp/issues/261)) ([f8b4bf7](https://github.com/libp2p/js-libp2p-tcp/commit/f8b4bf757e650bda232f1f6f21506212427db9f4)) + +## [6.2.1](https://github.com/libp2p/js-libp2p-tcp/compare/v6.2.0...v6.2.1) (2023-04-14) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#260](https://github.com/libp2p/js-libp2p-tcp/issues/260)) ([c0f3ec2](https://github.com/libp2p/js-libp2p-tcp/commit/c0f3ec286b19a9c6cd65731fca3d76c488cbf0a9)) + +## [6.2.0](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.6...v6.2.0) (2023-04-12) + + +### Features + +* add socket backlog option ([#263](https://github.com/libp2p/js-libp2p-tcp/issues/263)) ([8dba9c7](https://github.com/libp2p/js-libp2p-tcp/commit/8dba9c72e8cb2b5de545a9d1f25eb9988ace4cad)) + +## [6.1.6](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.5...v6.1.6) (2023-04-12) + + +### Bug Fixes + +* on MultiaddrConnection close() only create timer if needed ([#262](https://github.com/libp2p/js-libp2p-tcp/issues/262)) ([3637489](https://github.com/libp2p/js-libp2p-tcp/commit/36374895366eb82e321b021d3768a369c59ab3ba)) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#265](https://github.com/libp2p/js-libp2p-tcp/issues/265)) ([d2ef2d0](https://github.com/libp2p/js-libp2p-tcp/commit/d2ef2d09374b889907d5e9e68ffd566add31bf87)) + +## [6.1.5](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.4...v6.1.5) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#258](https://github.com/libp2p/js-libp2p-tcp/issues/258)) ([97e785f](https://github.com/libp2p/js-libp2p-tcp/commit/97e785f1d9d770d894d283acf532e94f4c479ccd)) + +## [6.1.4](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.3...v6.1.4) (2023-03-20) + + +### Dependencies + +* bump @multiformats/mafmt from 11.1.2 to 12.0.0 ([#257](https://github.com/libp2p/js-libp2p-tcp/issues/257)) ([2e8e534](https://github.com/libp2p/js-libp2p-tcp/commit/2e8e53417323d4e9f01d44879e948f449b157b9d)) + +## [6.1.3](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.2...v6.1.3) (2023-03-17) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([05bd31c](https://github.com/libp2p/js-libp2p-tcp/commit/05bd31c1cb224995f474af1541670ce5ca1fed09)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([bbd4c2a](https://github.com/libp2p/js-libp2p-tcp/commit/bbd4c2ac11e61e9da8e9cccf2eae5b70d7908ad4)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6dd008a](https://github.com/libp2p/js-libp2p-tcp/commit/6dd008ad2b3338c57903f22559ee9f741a7c7a8b)) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#256](https://github.com/libp2p/js-libp2p-tcp/issues/256)) ([048e9aa](https://github.com/libp2p/js-libp2p-tcp/commit/048e9aad8167069f0fb579c0710b56942347af67)) + +## [6.1.2](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.1...v6.1.2) (2023-02-01) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 8.0.5 to 9.1.1 ([#246](https://github.com/libp2p/js-libp2p-tcp/issues/246)) ([6f01bd1](https://github.com/libp2p/js-libp2p-tcp/commit/6f01bd1859eca7f5eccfb79b459424dd25e210a2)) +* **dev:** bump aegir from 37.12.1 to 38.1.0 ([#243](https://github.com/libp2p/js-libp2p-tcp/issues/243)) ([e7eea28](https://github.com/libp2p/js-libp2p-tcp/commit/e7eea28df2c315a62c8281955bd5cb3a6c3816d0)) + +## [6.1.1](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.0...v6.1.1) (2023-01-26) + + +### Bug Fixes + +* update log message when socket closes ([#249](https://github.com/libp2p/js-libp2p-tcp/issues/249)) ([f95bbf1](https://github.com/libp2p/js-libp2p-tcp/commit/f95bbf11aa59a184106ea1b1288442160dd0b373)) + +## [6.1.0](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.9...v6.1.0) (2023-01-21) + + +### Features + +* close server on maxConnections ([#218](https://github.com/libp2p/js-libp2p-tcp/issues/218)) ([bff54fa](https://github.com/libp2p/js-libp2p-tcp/commit/bff54fa5d40be44f421924e21b11d8c37fc53b1e)) + + +### Bug Fixes + +* specify host explicitly in node tests ([#247](https://github.com/libp2p/js-libp2p-tcp/issues/247)) ([d7e5a69](https://github.com/libp2p/js-libp2p-tcp/commit/d7e5a69d917850b756a0fd47760552e0b2fb0feb)) + +## [6.0.9](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.8...v6.0.9) (2023-01-17) + + +### Bug Fixes + +* increase default socket close timeout ([#242](https://github.com/libp2p/js-libp2p-tcp/issues/242)) ([a64ba41](https://github.com/libp2p/js-libp2p-tcp/commit/a64ba41f485f3bde28b58827c8a2ce5bf94f711a)), closes [#239](https://github.com/libp2p/js-libp2p-tcp/issues/239) + + +### Trivial Changes + +* replace err-code with CodeError ([#240](https://github.com/libp2p/js-libp2p-tcp/issues/240)) ([5c44562](https://github.com/libp2p/js-libp2p-tcp/commit/5c445628e97a1462c64a48253efd9ccb3441c399)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) + +## [6.0.8](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.7...v6.0.8) (2022-12-16) + + +### Documentation + +* fix build badge ([#238](https://github.com/libp2p/js-libp2p-tcp/issues/238)) ([8a94ced](https://github.com/libp2p/js-libp2p-tcp/commit/8a94cedc6e8806b597c650209b76b5ce38231146)) + +## [6.0.7](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.6...v6.0.7) (2022-12-15) + + +### Bug Fixes + +* publish tsdocs for this module ([#236](https://github.com/libp2p/js-libp2p-tcp/issues/236)) ([b4f88e7](https://github.com/libp2p/js-libp2p-tcp/commit/b4f88e7bfbe865eb00cfb1d99a4231b072b458a5)) + +## [6.0.6](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.5...v6.0.6) (2022-12-13) + + +### Bug Fixes + +* remove abortable-iterator and close socket directly on abort ([#220](https://github.com/libp2p/js-libp2p-tcp/issues/220)) ([28fe750](https://github.com/libp2p/js-libp2p-tcp/commit/28fe7500fa99c91f4f81d73671e885955b5d7e4a)) + +## [6.0.5](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.4...v6.0.5) (2022-12-06) + + +### Dependencies + +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#233](https://github.com/libp2p/js-libp2p-tcp/issues/233)) ([72a79ab](https://github.com/libp2p/js-libp2p-tcp/commit/72a79ab81d79daaeb8a77656e98a19b70f132595)) + +## [6.0.4](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.3...v6.0.4) (2022-11-22) + + +### Bug Fixes + +* use labels to differentiate interfaces for metrics ([#230](https://github.com/libp2p/js-libp2p-tcp/issues/230)) ([6c4c316](https://github.com/libp2p/js-libp2p-tcp/commit/6c4c316d080cde679c11a784c22284d6e1912b94)) + +## [6.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.2...v6.0.3) (2022-11-22) + + +### Bug Fixes + +* make metrics interface a dep instead of a dev dep ([#231](https://github.com/libp2p/js-libp2p-tcp/issues/231)) ([876ca13](https://github.com/libp2p/js-libp2p-tcp/commit/876ca132aa2b307315148628681cddfa0828b3ac)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.1...v6.0.2) (2022-11-17) + + +### Bug Fixes + +* update metric names to follow prometheus naming guide ([#228](https://github.com/libp2p/js-libp2p-tcp/issues/228)) ([24c5b37](https://github.com/libp2p/js-libp2p-tcp/commit/24c5b37ab64429972f29af6ae4516c18232d1ff3)) + + +### Trivial Changes + +* add test for filtering unix socket address ([#229](https://github.com/libp2p/js-libp2p-tcp/issues/229)) ([efcfbb2](https://github.com/libp2p/js-libp2p-tcp/commit/efcfbb28a77192a489834c8b8ad832337539d62b)), closes [#132](https://github.com/libp2p/js-libp2p-tcp/issues/132) + +## [6.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.0...v6.0.1) (2022-11-16) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 7.1.0 to 8.0.1 ([#225](https://github.com/libp2p/js-libp2p-tcp/issues/225)) ([a271056](https://github.com/libp2p/js-libp2p-tcp/commit/a271056c8d8d179dd95399f9621d790a0f18b84a)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v5.0.2...v6.0.0) (2022-11-05) + + +### ⚠ BREAKING CHANGES + +* requires metrics interface v4 + +### Features + +* add metrics ([#223](https://github.com/libp2p/js-libp2p-tcp/issues/223)) ([c004357](https://github.com/libp2p/js-libp2p-tcp/commit/c0043577777181545eef925b50e28743cfd7a29d)), closes [#217](https://github.com/libp2p/js-libp2p-tcp/issues/217) + +## [5.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v5.0.1...v5.0.2) (2022-11-05) + + +### Bug Fixes + +* handle listen error ([#224](https://github.com/libp2p/js-libp2p-tcp/issues/224)) ([4125e9e](https://github.com/libp2p/js-libp2p-tcp/commit/4125e9eaa4d531dbcb0f2777149d1ca8fa9460a5)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v5.0.0...v5.0.1) (2022-10-17) + + +### Trivial Changes + +* **deps-dev:** bump it-all from 1.0.6 to 2.0.0 ([#222](https://github.com/libp2p/js-libp2p-tcp/issues/222)) ([fddebdf](https://github.com/libp2p/js-libp2p-tcp/commit/fddebdff3ab2056da78f9ec665e5005a659e9045)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v4.1.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#219](https://github.com/libp2p/js-libp2p-tcp/issues/219)) ([be2dbc3](https://github.com/libp2p/js-libp2p-tcp/commit/be2dbc3f674e9bce534dc92d93ad2739ed6d2bef)) + +## [4.1.0](https://github.com/libp2p/js-libp2p-tcp/compare/v4.0.2...v4.1.0) (2022-10-11) + + +### Features + +* add server.maxConnections option ([#213](https://github.com/libp2p/js-libp2p-tcp/issues/213)) ([99e88a4](https://github.com/libp2p/js-libp2p-tcp/commit/99e88a4d3122c46f06f69cfbe3f72a2279e2329f)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v4.0.1...v4.0.2) (2022-10-11) + + +### Bug Fixes + +* port listener to ES6 class syntax ([#214](https://github.com/libp2p/js-libp2p-tcp/issues/214)) ([af7b8e2](https://github.com/libp2p/js-libp2p-tcp/commit/af7b8e2bf48ec0c9f01e087e76bc9570dca05783)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v4.0.0...v4.0.1) (2022-10-07) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 4.0.3 to 6.0.0 ([#216](https://github.com/libp2p/js-libp2p-tcp/issues/216)) ([f224a5a](https://github.com/libp2p/js-libp2p-tcp/commit/f224a5a0e8b497317b2b9410bb60433647a3185a)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v3.1.2...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* **deps:** bump @libp2p/interface-transport from 1.0.4 to 2.0.0 (#215) + +### Trivial Changes + +* **deps:** bump @libp2p/interface-transport from 1.0.4 to 2.0.0 ([#215](https://github.com/libp2p/js-libp2p-tcp/issues/215)) ([1adf73d](https://github.com/libp2p/js-libp2p-tcp/commit/1adf73db4e88e0c196766588a2972a3a6e28e69a)) + +## [3.1.2](https://github.com/libp2p/js-libp2p-tcp/compare/v3.1.1...v3.1.2) (2022-09-24) + + +### Bug Fixes + +* expose extra options from net.connect for dial and listen ([#211](https://github.com/libp2p/js-libp2p-tcp/issues/211)) ([6401a87](https://github.com/libp2p/js-libp2p-tcp/commit/6401a87080c77c36cf80634ee3cbe7c1c6fa89bb)) + +## [3.1.1](https://github.com/libp2p/js-libp2p-tcp/compare/v3.1.0...v3.1.1) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([5096725](https://github.com/libp2p/js-libp2p-tcp/commit/5096725d84fc0197cf0e055bfd6954a125001b47)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.8...v3.1.0) (2022-09-14) + + +### Features + +* Unix domain sockets ([#208](https://github.com/libp2p/js-libp2p-tcp/issues/208)) ([223f79b](https://github.com/libp2p/js-libp2p-tcp/commit/223f79b53091c33d4588b3c5e5adf28bcb929d97)), closes [#132](https://github.com/libp2p/js-libp2p-tcp/issues/132) + +## [3.0.8](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.7...v3.0.8) (2022-09-12) + + +### Trivial Changes + +* fix readme example ([#207](https://github.com/libp2p/js-libp2p-tcp/issues/207)) ([9f7cf76](https://github.com/libp2p/js-libp2p-tcp/commit/9f7cf761af41c12ca0a23c7df9b305ae876439f8)) + +## [3.0.7](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.6...v3.0.7) (2022-09-12) + + +### Bug Fixes + +* handle address being undefined ([#209](https://github.com/libp2p/js-libp2p-tcp/issues/209)) ([64ed009](https://github.com/libp2p/js-libp2p-tcp/commit/64ed0090ae9d6295496cd69e82b879627ea2069c)) + +## [3.0.6](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.5...v3.0.6) (2022-09-01) + + +### Bug Fixes + +* add socket keepalive ([#205](https://github.com/libp2p/js-libp2p-tcp/issues/205)) ([9ac799b](https://github.com/libp2p/js-libp2p-tcp/commit/9ac799b0c057d209920997a30ca219ea96000131)), closes [/github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go#L85](https://github.com/libp2p//github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go/issues/L85) [/github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go#L191](https://github.com/libp2p//github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go/issues/L191) + +## [3.0.5](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.4...v3.0.5) (2022-08-31) + + +### Bug Fixes + +* destroy sockets on close ([#204](https://github.com/libp2p/js-libp2p-tcp/issues/204)) ([e8b8f2e](https://github.com/libp2p/js-libp2p-tcp/commit/e8b8f2eaf547640f2566b18a8d061912965f2a55)), closes [#201](https://github.com/libp2p/js-libp2p-tcp/issues/201) + +## [3.0.4](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.3...v3.0.4) (2022-08-30) + + +### Bug Fixes + +* add tests for ipv6 wildcard support ([#203](https://github.com/libp2p/js-libp2p-tcp/issues/203)) ([71c974d](https://github.com/libp2p/js-libp2p-tcp/commit/71c974deaf40a4ab2eabc7d3e404d865a6892b20)), closes [#100](https://github.com/libp2p/js-libp2p-tcp/issues/100) + +## [3.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.2...v3.0.3) (2022-08-10) + + +### Bug Fixes + +* update all deps ([#199](https://github.com/libp2p/js-libp2p-tcp/issues/199)) ([e3b1344](https://github.com/libp2p/js-libp2p-tcp/commit/e3b13441199ef84912d8ba14f7e42235313b12d6)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.1...v3.0.2) (2022-07-13) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 2.1.0 to 3.0.1 ([#193](https://github.com/libp2p/js-libp2p-tcp/issues/193)) ([0571339](https://github.com/libp2p/js-libp2p-tcp/commit/0571339387dad55739fa2d831d39d5165620ecbb)) +* **deps:** bump @libp2p/utils from 2.0.1 to 3.0.0 ([#192](https://github.com/libp2p/js-libp2p-tcp/issues/192)) ([0c757ff](https://github.com/libp2p/js-libp2p-tcp/commit/0c757ff95b13bdf1ffb3285fa4b10d0988e8e42b)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.0...v3.0.1) (2022-06-27) + + +### Bug Fixes + +* add explicit return type to dial ([#191](https://github.com/libp2p/js-libp2p-tcp/issues/191)) ([cdb0932](https://github.com/libp2p/js-libp2p-tcp/commit/cdb09323188e77dee06a186ae43ba544faea5f86)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v2.0.1...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* the registry API has changed + +### Trivial Changes + +* **deps:** bump @libp2p/utils from 1.0.10 to 2.0.0 ([#187](https://github.com/libp2p/js-libp2p-tcp/issues/187)) ([fa59390](https://github.com/libp2p/js-libp2p-tcp/commit/fa593907832f19b77f86e9ae45389fee3d6228f8)) +* update deps ([#189](https://github.com/libp2p/js-libp2p-tcp/issues/189)) ([719f3f5](https://github.com/libp2p/js-libp2p-tcp/commit/719f3f55dc932afdf7d83c9d9a2cbf43146002a2)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v2.0.0...v2.0.1) (2022-06-16) + + +### Trivial Changes + +* **deps:** bump @libp2p/logger from 1.1.6 to 2.0.0 ([#186](https://github.com/libp2p/js-libp2p-tcp/issues/186)) ([5de2104](https://github.com/libp2p/js-libp2p-tcp/commit/5de2104f187e9f414b52fe2fee7580b78f2050af)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.11...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#184](https://github.com/libp2p/js-libp2p-tcp/issues/184)) ([2924414](https://github.com/libp2p/js-libp2p-tcp/commit/2924414995356ce179da315fde8fda0958959e2d)) + +### [1.0.11](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.10...v1.0.11) (2022-05-23) + + +### Bug Fixes + +* update interfaces ([#182](https://github.com/libp2p/js-libp2p-tcp/issues/182)) ([4ce3476](https://github.com/libp2p/js-libp2p-tcp/commit/4ce34767fe29355a5ff0c8069caa33b923ea9966)) + +### [1.0.10](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.9...v1.0.10) (2022-05-23) + + +### Trivial Changes + +* **deps-dev:** bump sinon from 13.0.2 to 14.0.0 ([#179](https://github.com/libp2p/js-libp2p-tcp/issues/179)) ([ed4c6bd](https://github.com/libp2p/js-libp2p-tcp/commit/ed4c6bd4ed8b9b01e86cbb5de0206b77683f1b93)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.8...v1.0.9) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#178](https://github.com/libp2p/js-libp2p-tcp/issues/178)) ([2b1e875](https://github.com/libp2p/js-libp2p-tcp/commit/2b1e8751493aefd3b9149e660357f73377ab5d0b)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.7...v1.0.8) (2022-04-07) + + +### Trivial Changes + +* update aegir ([#177](https://github.com/libp2p/js-libp2p-tcp/issues/177)) ([f54d6b4](https://github.com/libp2p/js-libp2p-tcp/commit/f54d6b4f631f26b248231330b4b8f512996625b3)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.6...v1.0.7) (2022-04-05) + + +### Trivial Changes + +* update README to the latest changes ([#169](https://github.com/libp2p/js-libp2p-tcp/issues/169)) ([129bf19](https://github.com/libp2p/js-libp2p-tcp/commit/129bf1958be3c2001b1f84646b6a7ccba6e20b52)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.5...v1.0.6) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#172](https://github.com/libp2p/js-libp2p-tcp/issues/172)) ([d72f629](https://github.com/libp2p/js-libp2p-tcp/commit/d72f629f7f89853bd849fb9123a99a407140d977)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.4...v1.0.5) (2022-02-21) + + +### Bug Fixes + +* update to latest interfaces ([#170](https://github.com/libp2p/js-libp2p-tcp/issues/170)) ([d8840e8](https://github.com/libp2p/js-libp2p-tcp/commit/d8840e881d5025b5c704eeb461a7e78685e53a87)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.3...v1.0.4) (2022-02-10) + + +### Bug Fixes + +* update to latest interfaces ([#168](https://github.com/libp2p/js-libp2p-tcp/issues/168)) ([c19462f](https://github.com/libp2p/js-libp2p-tcp/commit/c19462fccd2f8f1038530a0b3aea834fc9aab04d)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.2...v1.0.3) (2022-02-06) + + +### Bug Fixes + +* publish from root dir ([#167](https://github.com/libp2p/js-libp2p-tcp/issues/167)) ([4be1369](https://github.com/libp2p/js-libp2p-tcp/commit/4be13699a40e0b825c464e27ae3a6e6743e45c0e)) +* update to latest interfaces ([#164](https://github.com/libp2p/js-libp2p-tcp/issues/164)) ([7edad3c](https://github.com/libp2p/js-libp2p-tcp/commit/7edad3c4ae31d2bcc2ccebe2b5597da246c733c7)) + + +### Trivial Changes + +* update readme ([#166](https://github.com/libp2p/js-libp2p-tcp/issues/166)) ([2a0b609](https://github.com/libp2p/js-libp2p-tcp/commit/2a0b6098bb4739758e66f15a81ac42c922ea57b5)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.1...v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add dependabot ([#154](https://github.com/libp2p/js-libp2p-tcp/issues/154)) ([02974b3](https://github.com/libp2p/js-libp2p-tcp/commit/02974b3e41a1618a9ffb8e4a03edecc1c7fcd1f5)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.0...v1.0.1) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#155](https://github.com/libp2p/js-libp2p-tcp/issues/155)) ([def9ad7](https://github.com/libp2p/js-libp2p-tcp/commit/def9ad759d39da21639358b06bd847ab30b3cb7b)) + +## [0.17.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.17.1...v0.17.2) (2021-09-03) + + +### Bug Fixes + +* ts declaration export ([#150](https://github.com/libp2p/js-libp2p-tcp/issues/150)) ([d165fe5](https://github.com/libp2p/js-libp2p-tcp/commit/d165fe57960e2bb4a5324c372ef459c31dee1cf5)) + + + +## [0.17.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.17.0...v0.17.1) (2021-07-08) + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.16.0...v0.17.0) (2021-07-07) + + +### chore + +* update deps ([#147](https://github.com/libp2p/js-libp2p-tcp/issues/147)) ([b3e315a](https://github.com/libp2p/js-libp2p-tcp/commit/b3e315a6988cd4be7978e8922f275e525463bc0c)) + + +### BREAKING CHANGES + +* uses new majors of multiaddr and mafmt + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.4...v0.16.0) (2021-06-10) + + +### Features + +* add types ([#145](https://github.com/libp2p/js-libp2p-tcp/issues/145)) ([3249e02](https://github.com/libp2p/js-libp2p-tcp/commit/3249e0292b2ef5d818fe428ce61f689b25060d85)) + + + +## [0.15.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.2...v0.15.4) (2021-04-12) + + +### Bug Fixes + +* hanging close promise ([#140](https://github.com/libp2p/js-libp2p-tcp/issues/140)) ([3813100](https://github.com/libp2p/js-libp2p-tcp/commit/381310043852a9213f1abb62f5f0a7046d806286)) + + + + +## [0.15.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.2...v0.15.3) (2021-02-03) + + +### Bug Fixes + +* hanging close promise ([#140](https://github.com/libp2p/js-libp2p-tcp/issues/140)) ([3813100](https://github.com/libp2p/js-libp2p-tcp/commit/3813100)) + + + + +## [0.15.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.2...v0.15.2) (2020-12-28) + + +### Bug Fixes + +* catch error from maConn.close ([#128](https://github.com/libp2p/js-libp2p-tcp/issues/128)) ([0fe0815](https://github.com/libp2p/js-libp2p-tcp/commit/0fe0815)) +* catch thrown maConn errors in listener ([#122](https://github.com/libp2p/js-libp2p-tcp/issues/122)) ([86db568](https://github.com/libp2p/js-libp2p-tcp/commit/86db568)), closes [#121](https://github.com/libp2p/js-libp2p-tcp/issues/121) +* intermittent error when asking for interfaces ([#137](https://github.com/libp2p/js-libp2p-tcp/issues/137)) ([af9804e](https://github.com/libp2p/js-libp2p-tcp/commit/af9804e)) +* remove use of assert module ([#123](https://github.com/libp2p/js-libp2p-tcp/issues/123)) ([6272876](https://github.com/libp2p/js-libp2p-tcp/commit/6272876)) +* transport should not handle connection if upgradeInbound throws ([#119](https://github.com/libp2p/js-libp2p-tcp/issues/119)) ([21f8747](https://github.com/libp2p/js-libp2p-tcp/commit/21f8747)) + + +### Chores + +* update deps ([#134](https://github.com/libp2p/js-libp2p-tcp/issues/134)) ([d9f9912](https://github.com/libp2p/js-libp2p-tcp/commit/d9f9912)) + + +### BREAKING CHANGES + +* - The multiaddr dep used by this module returns Uint8Arrays and may + not be compatible with previous versions + +* chore: update utils + +* chore: remove gh dep url + + + + +## [0.15.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.0...v0.15.1) (2020-08-11) + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.6...v0.15.0) (2020-08-07) + + +### Chores + +* update deps ([#134](https://github.com/libp2p/js-libp2p-tcp/issues/134)) ([d9f9912](https://github.com/libp2p/js-libp2p-tcp/commit/d9f9912)) + + +### BREAKING CHANGES + +* - The multiaddr dep used by this module returns Uint8Arrays and may + not be compatible with previous versions + +* chore: update utils + +* chore: remove gh dep url + + + + +## [0.14.6](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.5...v0.14.6) (2020-07-17) + + + + +## [0.14.5](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.4...v0.14.5) (2020-04-28) + + +### Bug Fixes + +* catch error from maConn.close ([#128](https://github.com/libp2p/js-libp2p-tcp/issues/128)) ([0fe0815](https://github.com/libp2p/js-libp2p-tcp/commit/0fe0815)) + + + + +## [0.14.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.3...v0.14.4) (2020-02-24) + + +### Bug Fixes + +* catch thrown maConn errors in listener ([#122](https://github.com/libp2p/js-libp2p-tcp/issues/122)) ([86db568](https://github.com/libp2p/js-libp2p-tcp/commit/86db568)), closes [#121](https://github.com/libp2p/js-libp2p-tcp/issues/121) +* remove use of assert module ([#123](https://github.com/libp2p/js-libp2p-tcp/issues/123)) ([6272876](https://github.com/libp2p/js-libp2p-tcp/commit/6272876)) + + + + +## [0.14.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.2...v0.14.3) (2019-12-20) + + +### Bug Fixes + +* transport should not handle connection if upgradeInbound throws ([#119](https://github.com/libp2p/js-libp2p-tcp/issues/119)) ([21f8747](https://github.com/libp2p/js-libp2p-tcp/commit/21f8747)) + + + + +## [0.14.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.1...v0.14.2) (2019-12-06) + + +### Bug Fixes + +* **log:** log the bound port and host ([#117](https://github.com/libp2p/js-libp2p-tcp/issues/117)) ([7702646](https://github.com/libp2p/js-libp2p-tcp/commit/7702646)) + + +### Features + +* add path multiaddr support ([#118](https://github.com/libp2p/js-libp2p-tcp/issues/118)) ([d76a1f2](https://github.com/libp2p/js-libp2p-tcp/commit/d76a1f2)) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.0...v0.14.1) (2019-09-20) + + +### Bug Fixes + +* ensure timeline.close is set ([#113](https://github.com/libp2p/js-libp2p-tcp/issues/113)) ([605ee27](https://github.com/libp2p/js-libp2p-tcp/commit/605ee27)) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.13.1...v0.14.0) (2019-09-16) + + +### Features + +* change api to async / await ([#112](https://github.com/libp2p/js-libp2p-tcp/issues/112)) ([cf7d1b8](https://github.com/libp2p/js-libp2p-tcp/commit/cf7d1b8)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await. The API has also been updated according to the latest `interface-transport` version, https://github.com/libp2p/interface-transport/tree/v0.6.0#api. + + + + +## [0.13.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.13.0...v0.13.1) (2019-08-08) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.12.1...v0.13.0) (2018-09-12) + + +### Features + +* add support for dialing over dns ([eba0b48](https://github.com/libp2p/js-libp2p-tcp/commit/eba0b48)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.12.0...v0.12.1) (2018-07-31) + + +### Bug Fixes + +* invalid ip address and daemon can be crashed by remote user ([4b04b17](https://github.com/libp2p/js-libp2p-tcp/commit/4b04b17)) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.6...v0.12.0) (2018-04-05) + + +### Features + +* add class-is module ([ded1f68](https://github.com/libp2p/js-libp2p-tcp/commit/ded1f68)) + + + + +## [0.11.6](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.5...v0.11.6) (2018-02-20) + + + + +## [0.11.5](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.4...v0.11.5) (2018-02-07) + + + + +## [0.11.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.3...v0.11.4) (2018-02-07) + + + + +## [0.11.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.2...v0.11.3) (2018-02-07) + + +### Bug Fixes + +* clearing timeout when closes ([#87](https://github.com/libp2p/js-libp2p-tcp/issues/87)) ([f8f5266](https://github.com/libp2p/js-libp2p-tcp/commit/f8f5266)) + + + + +## [0.11.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.1...v0.11.2) (2018-01-12) + + +### Bug Fixes + +* missing dependency debug, fixes [#84](https://github.com/libp2p/js-libp2p-tcp/issues/84) ([74a88f6](https://github.com/libp2p/js-libp2p-tcp/commit/74a88f6)) + + + + +## [0.11.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.0...v0.11.1) (2017-10-13) + + +### Features + +* relay filtering ([11c4f45](https://github.com/libp2p/js-libp2p-tcp/commit/11c4f45)) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.10.2...v0.11.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#82](https://github.com/libp2p/js-libp2p-tcp/issues/82)) ([a54bb83](https://github.com/libp2p/js-libp2p-tcp/commit/a54bb83)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.10.1...v0.10.2) (2017-07-22) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.10.0...v0.10.1) (2017-04-13) + + +### Bug Fixes + +* catch errors on incomming sockets ([e204517](https://github.com/libp2p/js-libp2p-tcp/commit/e204517)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.4...v0.10.0) (2017-03-27) + + +### Bug Fixes + +* **dial:** proper error handling on dial ([#77](https://github.com/libp2p/js-libp2p-tcp/issues/77)) ([4d4f295](https://github.com/libp2p/js-libp2p-tcp/commit/4d4f295)) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.3...v0.9.4) (2017-03-21) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.2...v0.9.3) (2017-02-09) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.1...v0.9.2) (2017-02-09) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.0...v0.9.1) (2016-11-03) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.8.1...v0.9.0) (2016-11-03) + + +### Bug Fixes + +* **deps:** remove unused pull dep ([06689e3](https://github.com/libp2p/js-libp2p-tcp/commit/06689e3)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.8.0...v0.8.1) (2016-09-06) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.4...v0.8.0) (2016-09-06) + + +### Features + +* **deps:** update to published deps ([da8ee21](https://github.com/libp2p/js-libp2p-tcp/commit/da8ee21)) +* **pull:** migration to pull-streams ([5e89a26](https://github.com/libp2p/js-libp2p-tcp/commit/5e89a26)) +* **readme:** add pull-streams documentation ([d9f65e0](https://github.com/libp2p/js-libp2p-tcp/commit/d9f65e0)) + + + + +## [0.7.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.3...v0.7.4) (2016-08-03) + + + + +## [0.7.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.2...v0.7.3) (2016-06-26) + + + + +## [0.7.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.1...v0.7.2) (2016-06-23) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.0...v0.7.1) (2016-06-23) + + +### Bug Fixes + +* error was passed in duplicate ([9ac5cca](https://github.com/libp2p/js-libp2p-tcp/commit/9ac5cca)) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.6.2...v0.7.0) (2016-06-22) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.6.1...v0.6.2) (2016-06-01) + + +### Bug Fixes + +* address cr ([2ed01e8](https://github.com/libp2p/js-libp2p-tcp/commit/2ed01e8)) +* destroy hanging connections after timeout ([4a12169](https://github.com/libp2p/js-libp2p-tcp/commit/4a12169)) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.6.0...v0.6.1) (2016-05-29) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.3...v0.6.0) (2016-05-22) + + + + +## [0.5.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.2...v0.5.3) (2016-05-22) + + + + +## [0.5.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.1...v0.5.2) (2016-05-09) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.0...v0.5.1) (2016-05-08) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.4.0...v0.5.0) (2016-04-25) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.3.0...v0.4.0) (2016-03-14) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.2.1...v0.3.0) (2016-03-10) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.2.0...v0.2.1) (2016-03-04) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.1.2...v0.2.0) (2016-03-04) + + + + +## [0.1.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.1.1...v0.1.2) (2015-10-29) + + + + +## [0.1.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.1.0...v0.1.1) (2015-09-17) + + + + +# 0.1.0 (2015-09-16) diff --git a/packages/transport-tcp/LICENSE b/packages/transport-tcp/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-tcp/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-tcp/LICENSE-APACHE b/packages/transport-tcp/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-tcp/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-tcp/LICENSE-MIT b/packages/transport-tcp/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-tcp/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/transport-tcp/README.md b/packages/transport-tcp/README.md new file mode 100644 index 0000000000..83fe40f6fc --- /dev/null +++ b/packages/transport-tcp/README.md @@ -0,0 +1,87 @@ +# @libp2p/tcp + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> A TCP transport for libp2p + +## Table of contents + +- [Install](#install) +- [Usage](#usage) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/tcp +``` + +## Usage + +```js +import { tcp } from '@libp2p/tcp' +import { multiaddr } from '@multiformats/multiaddr' +import { pipe } from 'it-pipe' +import all from 'it-all' + +// A simple upgrader that just returns the MultiaddrConnection +const upgrader = { + upgradeInbound: async maConn => maConn, + upgradeOutbound: async maConn => maConn +} + +const transport = tcp()() + +const listener = transport.createListener({ + upgrader, + handler: (socket) => { + console.log('new connection opened') + pipe( + ['hello', ' ', 'World!'], + socket + ) + } +}) + +const addr = multiaddr('/ip4/127.0.0.1/tcp/9090') +await listener.listen(addr) +console.log('listening') + +const socket = await transport.dial(addr, { upgrader }) +const values = await pipe( + socket, + all +) +console.log(`Value: ${values.toString()}`) + +// Close connection after reading +await listener.close() +``` + +Outputs: + +```sh +listening +new connection opened +Value: hello World! +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-tcp/package.json b/packages/transport-tcp/package.json new file mode 100644 index 0000000000..3169f9a5b3 --- /dev/null +++ b/packages/transport-tcp/package.json @@ -0,0 +1,77 @@ +{ + "name": "@libp2p/tcp", + "version": "7.0.3", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "TCP", + "libp2p", + "network", + "p2p", + "peer", + "peer-to-peer" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t node -t electron-main", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", + "@types/sinon": "^10.0.15", + "stream-to-it": "^0.2.2" + }, + "devDependencies": { + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/interface-transport-compliance-tests": "^4.0.0", + "aegir": "^39.0.10", + "it-all": "^3.0.1", + "it-pipe": "^3.0.1", + "p-defer": "^4.0.0", + "sinon": "^15.1.0", + "uint8arrays": "^4.0.3" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-tcp/src/constants.ts b/packages/transport-tcp/src/constants.ts new file mode 100644 index 0000000000..8402e3a703 --- /dev/null +++ b/packages/transport-tcp/src/constants.ts @@ -0,0 +1,10 @@ +// p2p multi-address code +export const CODE_P2P = 421 +export const CODE_CIRCUIT = 290 +export const CODE_UNIX = 400 + +// Time to wait for a connection to close gracefully before destroying it manually +export const CLOSE_TIMEOUT = 2000 + +// Close the socket if there is no activity after this long in ms +export const SOCKET_TIMEOUT = 5 * 60000 // 5 mins diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts new file mode 100644 index 0000000000..71b50114e2 --- /dev/null +++ b/packages/transport-tcp/src/index.ts @@ -0,0 +1,252 @@ +import net from 'net' +import { type CreateListenerOptions, type DialOptions, type Listener, symbol, type Transport } from '@libp2p/interface-transport' +import { AbortError, CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import * as mafmt from '@multiformats/mafmt' +import { CODE_CIRCUIT, CODE_P2P, CODE_UNIX } from './constants.js' +import { type CloseServerOnMaxConnectionsOpts, TCPListener } from './listener.js' +import { toMultiaddrConnection } from './socket-to-conn.js' +import { multiaddrToNetConfig } from './utils.js' +import type { Connection } from '@libp2p/interface-connection' +import type { CounterGroup, Metrics } from '@libp2p/interface-metrics' +import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr' +import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net' + +const log = logger('libp2p:tcp') + +export interface TCPOptions { + /** + * An optional number in ms that is used as an inactivity timeout after which the socket will be closed + */ + inboundSocketInactivityTimeout?: number + + /** + * An optional number in ms that is used as an inactivity timeout after which the socket will be closed + */ + outboundSocketInactivityTimeout?: number + + /** + * When closing a socket, wait this long for it to close gracefully before it is closed more forcibly + */ + socketCloseTimeout?: number + + /** + * Set this property to reject connections when the server's connection count gets high. + * https://nodejs.org/api/net.html#servermaxconnections + */ + maxConnections?: number + + /** + * Parameter to specify the maximum length of the queue of pending connections + * https://nodejs.org/dist/latest-v18.x/docs/api/net.html#serverlisten + */ + backlog?: number + + /** + * Close server (stop listening for new connections) if connections exceed a limit. + * Open server (start listening for new connections) if connections fall below a limit. + */ + closeServerOnMaxConnections?: CloseServerOnMaxConnectionsOpts +} + +/** + * Expose a subset of net.connect options + */ +export interface TCPSocketOptions extends AbortOptions { + noDelay?: boolean + keepAlive?: boolean + keepAliveInitialDelay?: number + allowHalfOpen?: boolean +} + +export interface TCPDialOptions extends DialOptions, TCPSocketOptions { + +} + +export interface TCPCreateListenerOptions extends CreateListenerOptions, TCPSocketOptions { + +} + +export interface TCPComponents { + metrics?: Metrics +} + +export interface TCPMetrics { + dialerEvents: CounterGroup +} + +class TCP implements Transport { + private readonly opts: TCPOptions + private readonly metrics?: TCPMetrics + private readonly components: TCPComponents + + constructor (components: TCPComponents, options: TCPOptions = {}) { + this.opts = options + this.components = components + + if (components.metrics != null) { + this.metrics = { + dialerEvents: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', { + label: 'event', + help: 'Total count of TCP dialer events by type' + }) + } + } + } + + readonly [symbol] = true + + readonly [Symbol.toStringTag] = '@libp2p/tcp' + + async dial (ma: Multiaddr, options: TCPDialOptions): Promise { + options.keepAlive = options.keepAlive ?? true + + // options.signal destroys the socket before 'connect' event + const socket = await this._connect(ma, options) + + // Avoid uncaught errors caused by unstable connections + socket.on('error', err => { + log('socket error', err) + }) + + const maConn = toMultiaddrConnection(socket, { + remoteAddr: ma, + socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout, + socketCloseTimeout: this.opts.socketCloseTimeout, + metrics: this.metrics?.dialerEvents + }) + + const onAbort = (): void => { + maConn.close().catch(err => { + log.error('Error closing maConn after abort', err) + }) + } + options.signal?.addEventListener('abort', onAbort, { once: true }) + + log('new outbound connection %s', maConn.remoteAddr) + const conn = await options.upgrader.upgradeOutbound(maConn) + log('outbound connection %s upgraded', maConn.remoteAddr) + + options.signal?.removeEventListener('abort', onAbort) + + if (options.signal?.aborted === true) { + conn.close().catch(err => { + log.error('Error closing conn after abort', err) + }) + + throw new AbortError() + } + + return conn + } + + async _connect (ma: Multiaddr, options: TCPDialOptions): Promise { + if (options.signal?.aborted === true) { + throw new AbortError() + } + + return new Promise((resolve, reject) => { + const start = Date.now() + const cOpts = multiaddrToNetConfig(ma) as (IpcSocketConnectOpts & TcpSocketConnectOpts) + const cOptsStr = cOpts.path ?? `${cOpts.host ?? ''}:${cOpts.port}` + + log('dialing %j', cOpts) + const rawSocket = net.connect(cOpts) + + const onError = (err: Error): void => { + err.message = `connection error ${cOptsStr}: ${err.message}` + this.metrics?.dialerEvents.increment({ error: true }) + + done(err) + } + + const onTimeout = (): void => { + log('connection timeout %s', cOptsStr) + this.metrics?.dialerEvents.increment({ timeout: true }) + + const err = new CodeError(`connection timeout after ${Date.now() - start}ms`, 'ERR_CONNECT_TIMEOUT') + // Note: this will result in onError() being called + rawSocket.emit('error', err) + } + + const onConnect = (): void => { + log('connection opened %j', cOpts) + this.metrics?.dialerEvents.increment({ connect: true }) + done() + } + + const onAbort = (): void => { + log('connection aborted %j', cOpts) + this.metrics?.dialerEvents.increment({ abort: true }) + rawSocket.destroy() + done(new AbortError()) + } + + const done = (err?: any): void => { + rawSocket.removeListener('error', onError) + rawSocket.removeListener('timeout', onTimeout) + rawSocket.removeListener('connect', onConnect) + + if (options.signal != null) { + options.signal.removeEventListener('abort', onAbort) + } + + if (err != null) { + reject(err); return + } + + resolve(rawSocket) + } + + rawSocket.on('error', onError) + rawSocket.on('timeout', onTimeout) + rawSocket.on('connect', onConnect) + + if (options.signal != null) { + options.signal.addEventListener('abort', onAbort) + } + }) + } + + /** + * Creates a TCP listener. The provided `handler` function will be called + * anytime a new incoming Connection has been successfully upgraded via + * `upgrader.upgradeInbound`. + */ + createListener (options: TCPCreateListenerOptions): Listener { + return new TCPListener({ + ...options, + maxConnections: this.opts.maxConnections, + backlog: this.opts.backlog, + closeServerOnMaxConnections: this.opts.closeServerOnMaxConnections, + socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout, + socketCloseTimeout: this.opts.socketCloseTimeout, + metrics: this.components.metrics + }) + } + + /** + * Takes a list of `Multiaddr`s and returns only valid TCP addresses + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + return multiaddrs.filter(ma => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + if (ma.protoCodes().includes(CODE_UNIX)) { + return true + } + + return mafmt.TCP.matches(ma.decapsulateCode(CODE_P2P)) + }) + } +} + +export function tcp (init: TCPOptions = {}): (components?: TCPComponents) => Transport { + return (components: TCPComponents = {}) => { + return new TCP(components, init) + } +} diff --git a/packages/transport-tcp/src/listener.ts b/packages/transport-tcp/src/listener.ts new file mode 100644 index 0000000000..016f9e4228 --- /dev/null +++ b/packages/transport-tcp/src/listener.ts @@ -0,0 +1,334 @@ +import net from 'net' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { CODE_P2P } from './constants.js' +import { toMultiaddrConnection } from './socket-to-conn.js' +import { + getMultiaddrs, + multiaddrToNetConfig, + type NetConfig +} from './utils.js' +import type { TCPCreateListenerOptions } from './index.js' +import type { MultiaddrConnection, Connection } from '@libp2p/interface-connection' +import type { CounterGroup, MetricGroup, Metrics } from '@libp2p/interface-metrics' +import type { Upgrader, Listener, ListenerEvents } from '@libp2p/interface-transport' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:tcp:listener') + +/** + * Attempts to close the given maConn. If a failure occurs, it will be logged + */ +async function attemptClose (maConn: MultiaddrConnection): Promise { + try { + await maConn.close() + } catch (err) { + log.error('an error occurred closing the connection', err) + } +} + +export interface CloseServerOnMaxConnectionsOpts { + /** Server listens once connection count is less than `listenBelow` */ + listenBelow: number + /** Close server once connection count is greater than or equal to `closeAbove` */ + closeAbove: number + onListenError?: (err: Error) => void +} + +interface Context extends TCPCreateListenerOptions { + handler?: (conn: Connection) => void + upgrader: Upgrader + socketInactivityTimeout?: number + socketCloseTimeout?: number + maxConnections?: number + backlog?: number + metrics?: Metrics + 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 + listeningAddr: Multiaddr + peerId: string | null + netConfig: NetConfig +} + +export class TCPListener extends EventEmitter implements Listener { + 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 metrics?: TCPListenerMetrics + private addr: string + + constructor (private readonly context: Context) { + super() + + context.keepAlive = context.keepAlive ?? true + + this.addr = 'unknown' + this.server = net.createServer(context, this.onSocket.bind(this)) + + // https://nodejs.org/api/net.html#servermaxconnections + // If set reject connections when the server's connection count gets high + // Useful to prevent too resource exhaustion via many open connections on high bursts of activity + if (context.maxConnections !== undefined) { + this.server.maxConnections = context.maxConnections + } + + if (context.closeServerOnMaxConnections != null) { + // Sanity check options + if (context.closeServerOnMaxConnections.closeAbove < context.closeServerOnMaxConnections.listenBelow) { + throw Error('closeAbove must be >= listenBelow') + } + } + + this.server + .on('listening', () => { + if (context.metrics != null) { + // we are listening, register metrics for our port + const address = this.server.address() + + if (address == null) { + this.addr = 'unknown' + } else if (typeof address === 'string') { + // unix socket + this.addr = address + } else { + this.addr = `${address.address}:${address.port}` + } + + context.metrics?.registerMetricGroup('libp2p_tcp_inbound_connections_total', { + label: 'address', + help: 'Current active connections in TCP listener', + calculate: () => { + return { + [this.addr]: this.connections.size + } + } + }) + + this.metrics = { + status: context.metrics.registerMetricGroup('libp2p_tcp_listener_status_info', { + label: 'address', + help: 'Current status of the TCP listener socket' + }), + errors: context.metrics.registerMetricGroup('libp2p_tcp_listener_errors_total', { + label: 'address', + help: 'Total count of TCP listener errors by type' + }), + events: context.metrics.registerMetricGroup('libp2p_tcp_listener_events_total', { + label: 'address', + help: 'Total count of TCP listener events by type' + }) + } + + this.metrics?.status.update({ + [this.addr]: SERVER_STATUS_UP + }) + } + + this.dispatchEvent(new CustomEvent('listening')) + }) + .on('error', err => { + this.metrics?.errors.increment({ [`${this.addr} listen_error`]: true }) + this.dispatchEvent(new CustomEvent('error', { detail: err })) + }) + .on('close', () => { + this.metrics?.status.update({ + [this.addr]: SERVER_STATUS_DOWN + }) + this.dispatchEvent(new CustomEvent('close')) + }) + } + + private onSocket (socket: net.Socket): void { + // Avoid uncaught errors caused by unstable connections + socket.on('error', err => { + log('socket error', err) + this.metrics?.events.increment({ [`${this.addr} error`]: true }) + }) + + let maConn: MultiaddrConnection + try { + maConn = toMultiaddrConnection(socket, { + listeningAddr: this.status.started ? this.status.listeningAddr : undefined, + socketInactivityTimeout: this.context.socketInactivityTimeout, + socketCloseTimeout: this.context.socketCloseTimeout, + metrics: this.metrics?.events, + metricPrefix: `${this.addr} ` + }) + } catch (err) { + log.error('inbound connection failed', err) + this.metrics?.errors.increment({ [`${this.addr} inbound_to_connection`]: true }) + return + } + + log('new inbound connection %s', maConn.remoteAddr) + try { + this.context.upgrader.upgradeInbound(maConn) + .then((conn) => { + log('inbound connection upgraded %s', maConn.remoteAddr) + this.connections.add(maConn) + + socket.once('close', () => { + this.connections.delete(maConn) + + if ( + this.context.closeServerOnMaxConnections != null && + this.connections.size < this.context.closeServerOnMaxConnections.listenBelow + ) { + // 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 + // acts as an eventual retry mechanism. onListenError allows the consumer act on this. + this.netListen().catch(e => { + log.error('error attempting to listen server once connection count under limit', e) + this.context.closeServerOnMaxConnections?.onListenError?.(e as Error) + }) + } + }) + + if (this.context.handler != null) { + this.context.handler(conn) + } + + if ( + this.context.closeServerOnMaxConnections != null && + this.connections.size >= this.context.closeServerOnMaxConnections.closeAbove + ) { + this.netClose() + } + + this.dispatchEvent(new CustomEvent('connection', { detail: conn })) + }) + .catch(async err => { + log.error('inbound connection failed', err) + this.metrics?.errors.increment({ [`${this.addr} inbound_upgrade`]: true }) + + await attemptClose(maConn) + }) + .catch(err => { + log.error('closing inbound connection failed', err) + }) + } catch (err) { + log.error('inbound connection failed', err) + + attemptClose(maConn) + .catch(err => { + log.error('closing inbound connection failed', err) + this.metrics?.errors.increment({ [`${this.addr} inbound_closing_failed`]: true }) + }) + } + } + + getAddrs (): Multiaddr[] { + if (!this.status.started) { + return [] + } + + let addrs: Multiaddr[] = [] + const address = this.server.address() + const { listeningAddr, peerId } = this.status + + if (address == null) { + return [] + } + + if (typeof address === 'string') { + addrs = [listeningAddr] + } else { + try { + // Because TCP will only return the IPv6 version + // we need to capture from the passed multiaddr + if (listeningAddr.toString().startsWith('/ip4')) { + addrs = addrs.concat(getMultiaddrs('ip4', address.address, address.port)) + } else if (address.family === 'IPv6') { + addrs = addrs.concat(getMultiaddrs('ip6', address.address, address.port)) + } + } catch (err) { + log.error('could not turn %s:%s into multiaddr', address.address, address.port, err) + } + } + + return addrs.map(ma => peerId != null ? ma.encapsulate(`/p2p/${peerId}`) : ma) + } + + async listen (ma: Multiaddr): Promise { + if (this.status.started) { + throw Error('server is 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 }) + } + + await this.netListen() + } + + 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() + } + + private async netListen (): Promise { + if (!this.status.started || this.server.listening) { + return + } + + const netConfig = this.status.netConfig + + await new Promise((resolve, reject) => { + // NOTE: 'listening' event is only fired on success. Any error such as port already binded, is emitted via 'error' + this.server.once('error', reject) + this.server.listen(netConfig, resolve) + }) + + log('Listening on %s', this.server.address()) + } + + private netClose (): void { + if (!this.status.started || !this.server.listening) { + return + } + + log('Closing server on %s', this.server.address()) + + // NodeJS implementation tracks listening status with `this._handle` property. + // - Server.close() sets this._handle to null immediately. If this._handle is null, ERR_SERVER_NOT_RUNNING is thrown + // - Server.listening returns `this._handle !== null` https://github.com/nodejs/node/blob/386d761943bb1b217fba27d6b80b658c23009e60/lib/net.js#L1675 + // - Server.listen() if `this._handle !== null` throws ERR_SERVER_ALREADY_LISTEN + // + // NOTE: Both listen and close are technically not async actions, so it's not necessary to track + // states 'pending-close' or 'pending-listen' + + // From docs https://nodejs.org/api/net.html#serverclosecallback + // 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() + } +} diff --git a/packages/transport-tcp/src/socket-to-conn.ts b/packages/transport-tcp/src/socket-to-conn.ts new file mode 100644 index 0000000000..993776a831 --- /dev/null +++ b/packages/transport-tcp/src/socket-to-conn.ts @@ -0,0 +1,196 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr' +// @ts-expect-error no types +import toIterable from 'stream-to-it' +import { CLOSE_TIMEOUT, SOCKET_TIMEOUT } from './constants.js' +import { multiaddrToNetConfig } from './utils.js' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { CounterGroup } from '@libp2p/interface-metrics' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Socket } from 'net' + +const log = logger('libp2p:tcp:socket') + +interface ToConnectionOptions { + listeningAddr?: Multiaddr + remoteAddr?: Multiaddr + localAddr?: Multiaddr + socketInactivityTimeout?: number + socketCloseTimeout?: number + metrics?: CounterGroup + metricPrefix?: string +} + +/** + * Convert a socket into a MultiaddrConnection + * https://github.com/libp2p/interface-transport#multiaddrconnection + */ +export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions): MultiaddrConnection => { + const metrics = options.metrics + const metricPrefix = options.metricPrefix ?? '' + const inactivityTimeout = options.socketInactivityTimeout ?? SOCKET_TIMEOUT + const closeTimeout = options.socketCloseTimeout ?? CLOSE_TIMEOUT + + // Check if we are connected on a unix path + if (options.listeningAddr?.getPath() != null) { + options.remoteAddr = options.listeningAddr + } + + if (options.remoteAddr?.getPath() != null) { + options.localAddr = options.remoteAddr + } + + let remoteAddr: Multiaddr + + if (options.remoteAddr != null) { + remoteAddr = options.remoteAddr + } else { + if (socket.remoteAddress == null || socket.remotePort == null) { + // this can be undefined if the socket is destroyed (for example, if the client disconnected) + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketremoteaddress + throw new CodeError('Could not determine remote address or port', 'ERR_NO_REMOTE_ADDRESS') + } + + remoteAddr = toMultiaddr(socket.remoteAddress, socket.remotePort) + } + + const lOpts = multiaddrToNetConfig(remoteAddr) + const lOptsStr = lOpts.path ?? `${lOpts.host ?? ''}:${lOpts.port ?? ''}` + const { sink, source } = toIterable.duplex(socket) + + // by default there is no timeout + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback + socket.setTimeout(inactivityTimeout, () => { + log('%s socket read timeout', lOptsStr) + metrics?.increment({ [`${metricPrefix}timeout`]: true }) + + // only destroy with an error if the remote has not sent the FIN message + let err: Error | undefined + if (socket.readable) { + err = new CodeError('Socket read timeout', 'ERR_SOCKET_READ_TIMEOUT') + } + + // if the socket times out due to inactivity we must manually close the connection + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout + socket.destroy(err) + }) + + socket.once('close', () => { + log('%s socket close', lOptsStr) + metrics?.increment({ [`${metricPrefix}close`]: true }) + + // In instances where `close` was not explicitly called, + // such as an iterable stream ending, ensure we have set the close + // timeline + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + }) + + socket.once('end', () => { + // the remote sent a FIN packet which means no more data will be sent + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end + log('%s socket end', lOptsStr) + metrics?.increment({ [`${metricPrefix}end`]: true }) + }) + + const maConn: MultiaddrConnection = { + async sink (source) { + try { + await sink(source) + } catch (err: any) { + // If aborted we can safely ignore + if (err.type !== 'aborted') { + // If the source errored the socket will already have been destroyed by + // toIterable.duplex(). If the socket errored it will already be + // destroyed. There's nothing to do here except log the error & return. + log(err) + } + } + + // we have finished writing, send the FIN message + socket.end() + }, + + source, + + // If the remote address was passed, use it - it may have the peer ID encapsulated + remoteAddr, + + timeline: { open: Date.now() }, + + async close () { + if (socket.destroyed) { + log('%s socket was already destroyed when trying to close', lOptsStr) + return + } + + log('%s closing socket', lOptsStr) + await new Promise((resolve, reject) => { + const start = Date.now() + + let timeout: NodeJS.Timeout | undefined + + socket.once('close', () => { + log('%s socket closed', lOptsStr) + // socket completely closed + if (timeout !== undefined) { + clearTimeout(timeout) + } + resolve() + }) + socket.once('error', (err: Error) => { + log('%s socket error', lOptsStr, err) + + // error closing socket + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + + if (socket.destroyed) { + if (timeout !== undefined) { + clearTimeout(timeout) + } + } + + reject(err) + }) + + // shorten inactivity timeout + socket.setTimeout(closeTimeout) + + // close writable end of the socket + socket.end() + + if (socket.writableLength > 0) { + // Attempt to end the socket. If it takes longer to close than the + // timeout, destroy it manually. + timeout = setTimeout(() => { + if (socket.destroyed) { + log('%s is already destroyed', lOptsStr) + resolve() + } else { + log('%s socket close timeout after %dms, destroying it manually', lOptsStr, Date.now() - start) + + // will trigger 'error' and 'close' events that resolves promise + socket.destroy(new CodeError('Socket close timeout', 'ERR_SOCKET_CLOSE_TIMEOUT')) + } + }, closeTimeout).unref() + // there are outgoing bytes waiting to be sent + socket.once('drain', () => { + log('%s socket drained', lOptsStr) + + // all bytes have been sent we can destroy the socket (maybe) before the timeout + socket.destroy() + }) + } else { + // nothing to send, destroy immediately, no need the timeout + socket.destroy() + } + }) + } + } + + return maConn +} diff --git a/packages/transport-tcp/src/utils.ts b/packages/transport-tcp/src/utils.ts new file mode 100644 index 0000000000..7db5975031 --- /dev/null +++ b/packages/transport-tcp/src/utils.ts @@ -0,0 +1,53 @@ +import os from 'os' +import path from 'path' +import { multiaddr } from '@multiformats/multiaddr' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { ListenOptions, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net' + +const ProtoFamily = { ip4: 'IPv4', ip6: 'IPv6' } + +export type NetConfig = ListenOptions | (IpcSocketConnectOpts & TcpSocketConnectOpts) + +export function multiaddrToNetConfig (addr: Multiaddr, config: NetConfig = {}): NetConfig { + const listenPath = addr.getPath() + + // unix socket listening + if (listenPath != null) { + if (os.platform() === 'win32') { + // Use named pipes on Windows systems. + return { path: path.join('\\\\.\\pipe\\', listenPath) } + } else { + return { path: listenPath } + } + } + + // tcp listening + return { ...addr.toOptions(), ...config } +} + +export function getMultiaddrs (proto: 'ip4' | 'ip6', ip: string, port: number): Multiaddr[] { + const toMa = (ip: string): Multiaddr => multiaddr(`/${proto}/${ip}/tcp/${port}`) + return (isAnyAddr(ip) ? getNetworkAddrs(ProtoFamily[proto]) : [ip]).map(toMa) +} + +export function isAnyAddr (ip: string): boolean { + return ['0.0.0.0', '::'].includes(ip) +} + +const networks = os.networkInterfaces() + +function getNetworkAddrs (family: string): string[] { + const addresses: string[] = [] + + for (const [, netAddrs] of Object.entries(networks)) { + if (netAddrs != null) { + for (const netAddr of netAddrs) { + if (netAddr.family === family) { + addresses.push(netAddr.address) + } + } + } + } + + return addresses +} diff --git a/packages/transport-tcp/test/compliance.spec.ts b/packages/transport-tcp/test/compliance.spec.ts new file mode 100644 index 0000000000..7c6ba85808 --- /dev/null +++ b/packages/transport-tcp/test/compliance.spec.ts @@ -0,0 +1,42 @@ +import net from 'net' +import tests from '@libp2p/interface-transport-compliance-tests' +import { multiaddr } from '@multiformats/multiaddr' +import sinon from 'sinon' +import { tcp } from '../src/index.js' + +describe('interface-transport compliance', () => { + tests({ + async setup () { + const transport = tcp()() + const addrs = [ + multiaddr('/ip4/127.0.0.1/tcp/9091'), + multiaddr('/ip4/127.0.0.1/tcp/9092'), + multiaddr('/ip4/127.0.0.1/tcp/9093'), + multiaddr('/ip6/::/tcp/9094') + ] + + // Used by the dial tests to simulate a delayed connect + const connector = { + delay (delayMs: number) { + const netConnect = net.connect + sinon.replace(net, 'connect', (opts: any) => { + const socket = netConnect(opts) + const socketEmit = socket.emit.bind(socket) + sinon.replace(socket, 'emit', (...args: [string]) => { + const time = args[0] === 'connect' ? delayMs : 0 + setTimeout(() => socketEmit(...args), time) + return true + }) + return socket + }) + }, + restore () { + sinon.restore() + } + } + + return { transport, addrs, connector } + }, + async teardown () {} + }) +}) diff --git a/packages/transport-tcp/test/connection.spec.ts b/packages/transport-tcp/test/connection.spec.ts new file mode 100644 index 0000000000..67b1d23597 --- /dev/null +++ b/packages/transport-tcp/test/connection.spec.ts @@ -0,0 +1,91 @@ +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Transport, Upgrader } from '@libp2p/interface-transport' + +describe('valid localAddr and remoteAddr', () => { + let transport: Transport + let upgrader: Upgrader + + beforeEach(() => { + transport = tcp()() + upgrader = mockUpgrader({ + events: new EventEmitter() + }) + }) + + const ma = multiaddr('/ip4/127.0.0.1/tcp/0') + + it('should resolve port 0', async () => { + // Create a Promise that resolves when a connection is handled + let handled: (conn: Connection) => void + const handlerPromise = new Promise(resolve => { handled = resolve }) + + const handler = (conn: Connection): void => { handled(conn) } + + // Create a listener with the handler + const listener = transport.createListener({ + handler, + upgrader + }) + + // Listen on the multi-address + await listener.listen(ma) + + const localAddrs = listener.getAddrs() + expect(localAddrs.length).to.equal(1) + + // Dial to that address + await transport.dial(localAddrs[0], { + upgrader + }) + + // Wait for the incoming dial to be handled + await handlerPromise + + // Close the listener + await listener.close() + }) + + it('should handle multiple simultaneous closes', async () => { + // Create a Promise that resolves when a connection is handled + let handled: (conn: Connection) => void + const handlerPromise = new Promise(resolve => { handled = resolve }) + + const handler = (conn: Connection): void => { handled(conn) } + + // Create a listener with the handler + const listener = transport.createListener({ + handler, + upgrader + }) + + // Listen on the multi-address + await listener.listen(ma) + + const localAddrs = listener.getAddrs() + expect(localAddrs.length).to.equal(1) + + // Dial to that address + const dialerConn = await transport.dial(localAddrs[0], { + upgrader + }) + + // Wait for the incoming dial to be handled + await handlerPromise + + // Close the dialer with two simultaneous calls to `close` + await Promise.race([ + new Promise((resolve, reject) => setTimeout(() => { reject(new Error('Timed out waiting for connection close')) }, 500)), + await Promise.all([ + dialerConn.close(), + dialerConn.close() + ]) + ]) + + await listener.close() + }) +}) diff --git a/packages/transport-tcp/test/filter.spec.ts b/packages/transport-tcp/test/filter.spec.ts new file mode 100644 index 0000000000..ce345a4079 --- /dev/null +++ b/packages/transport-tcp/test/filter.spec.ts @@ -0,0 +1,42 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' +import type { Transport } from '@libp2p/interface-transport' + +describe('filter addrs', () => { + const base = '/ip4/127.0.0.1' + const ipfs = '/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw' + const unix = '/tmp/some/file.sock' + + let transport: Transport + + before(() => { + transport = tcp()() + }) + + it('filter valid addrs for this transport', () => { + const ma1 = multiaddr(base + '/tcp/9090') + const ma2 = multiaddr(base + '/udp/9090') + const ma3 = multiaddr(base + '/tcp/9090/http') + const ma4 = multiaddr(base + '/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma5 = multiaddr(base + '/tcp/9090/http' + ipfs) + const ma6 = multiaddr('/ip4/127.0.0.1/tcp/9090/p2p-circuit' + ipfs) + const ma7 = multiaddr('/dns4/libp2p.io/tcp/9090') + const ma8 = multiaddr('/dnsaddr/libp2p.io/tcp/9090') + const ma9 = multiaddr('/unix' + unix) + + const valid = transport.filter([ma1, ma2, ma3, ma4, ma5, ma6, ma7, ma8, ma9]) + expect(valid.length).to.equal(5) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma4) + expect(valid[4]).to.deep.equal(ma9) + }) + + it('filter a single addr for this transport', () => { + const ma1 = multiaddr(base + '/tcp/9090') + + const valid = transport.filter([ma1]) + expect(valid.length).to.equal(1) + expect(valid[0]).to.eql(ma1) + }) +}) diff --git a/packages/transport-tcp/test/listen-dial.spec.ts b/packages/transport-tcp/test/listen-dial.spec.ts new file mode 100644 index 0000000000..f1c54b852e --- /dev/null +++ b/packages/transport-tcp/test/listen-dial.spec.ts @@ -0,0 +1,389 @@ +import os from 'os' +import path from 'path' +import { mockRegistrar, mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pipe } from 'it-pipe' +import pDefer from 'p-defer' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { tcp } from '../src/index.js' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { Transport, Upgrader } from '@libp2p/interface-transport' + +const isCI = process.env.CI + +describe('listen', () => { + let transport: Transport + let listener: any + let upgrader: Upgrader + + beforeEach(() => { + transport = tcp()() + upgrader = mockUpgrader({ + events: new EventEmitter() + }) + }) + + afterEach(async () => { + try { + if (listener != null) { + await listener.close() + } + } catch { + // some tests close the listener so ignore errors + } + }) + + it('listen on path', async () => { + const mh = multiaddr(`/unix/${path.resolve(os.tmpdir(), `/tmp/p2pd-${Date.now()}.sock`)}`) + + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('listen on port 0', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('errors when listening on busy port', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const listener2 = transport.createListener({ + upgrader + }) + + const mh2 = listener.getAddrs()[0] + await expect(listener2.listen(mh2)).to.eventually.be.rejected() + .with.property('code', 'EADDRINUSE') + }) + + it('listen on IPv6 addr', async () => { + if (isCI != null) { + return + } + const mh = multiaddr('/ip6/::/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('listen on any Interface', async () => { + const mh = multiaddr('/ip4/0.0.0.0/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('getAddrs', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length).to.equal(1) + expect(multiaddrs[0]).to.deep.equal(mh) + }) + + it('getAddrs on port 0 listen', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length).to.equal(1) + }) + + it('getAddrs from listening on 0.0.0.0', async () => { + const mh = multiaddr('/ip4/0.0.0.0/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length > 0).to.equal(true) + expect(multiaddrs[0].toString().indexOf('0.0.0.0')).to.equal(-1) + }) + + it('getAddrs from listening on 0.0.0.0 and port 0', async () => { + const mh = multiaddr('/ip4/0.0.0.0/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length > 0).to.equal(true) + expect(multiaddrs[0].toString().indexOf('0.0.0.0')).to.equal(-1) + }) + + it('getAddrs from listening on ip6 \'::\'', async () => { + const mh = multiaddr('/ip6/::/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length > 0).to.equal(true) + expect(multiaddrs[0].toOptions().host).to.not.equal('::') + }) + + it('getAddrs preserves IPFS Id', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length).to.equal(1) + expect(multiaddrs[0]).to.deep.equal(mh) + }) +}) + +describe('dial', () => { + const protocol = '/echo/1.0.0' + let transport: Transport + let upgrader: Upgrader + + beforeEach(async () => { + const registrar = mockRegistrar() + void registrar.handle(protocol, (evt) => { + void pipe( + evt.stream, + evt.stream + ) + }) + upgrader = mockUpgrader({ + registrar, + events: new EventEmitter() + }) + + transport = tcp()() + }) + + it('dial on IPv4', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + await listener.close() + }) + + it('dial on IPv6', async () => { + if (isCI != null) { + return + } + + const ma = multiaddr('/ip6/::/tcp/9090') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + await listener.close() + }) + + it('dial on path', async () => { + const ma = multiaddr(`/unix/${path.resolve(os.tmpdir(), `/tmp/p2pd-${Date.now()}.sock`)}`) + + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + await listener.close() + }) + + it('dial and destroy on listener', async () => { + let handled: () => void + const handledPromise = new Promise(resolve => { handled = resolve }) + + const ma = multiaddr('/ip6/::/tcp/9090') + + const listener = transport.createListener({ + handler: (conn) => { + // let multistream select finish before closing + setTimeout(() => { + void conn.close() + .then(() => { handled() }) + }, 100) + }, + upgrader + }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + + const conn = await transport.dial(addrs[0], { + upgrader + }) + const stream = await conn.newStream([protocol]) + pipe(stream) + + await handledPromise + await conn.close() + await listener.close() + }) + + it('dial and destroy on dialer', async () => { + if (isCI != null) { + return + } + + let handled: () => void + const handledPromise = new Promise(resolve => { handled = resolve }) + + const ma = multiaddr('/ip6/::/tcp/9090') + + const listener = transport.createListener({ + handler: () => { + handled() + }, + upgrader + }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + const conn = await transport.dial(addrs[0], { + upgrader + }) + + await conn.close() + await handledPromise + await listener.close() + }) + + it('dials on IPv4 with IPFS Id', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + + await conn.close() + await listener.close() + }) + + it('aborts during dial', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const maConnPromise = pDefer() + + // @ts-expect-error missing return value + upgrader.upgradeOutbound = async (maConn) => { + maConnPromise.resolve(maConn) + + // take a long time to give us time to abort the dial + await new Promise((resolve) => { + setTimeout(() => { resolve() }, 100) + }) + } + + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const abortController = new AbortController() + + // abort once the upgrade process has started + void maConnPromise.promise.then(() => { abortController.abort() }) + + await expect(transport.dial(ma, { + upgrader, + signal: abortController.signal + })).to.eventually.be.rejected('The operation was aborted') + + await expect(maConnPromise.promise).to.eventually.have.nested.property('timeline.close') + .that.is.ok('did not gracefully close maConn') + + await listener.close() + }) + + it('aborts before dial', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const abortController = new AbortController() + abortController.abort() + + await expect(transport.dial(ma, { + upgrader, + signal: abortController.signal + })).to.eventually.be.rejected('The operation was aborted') + + await listener.close() + }) +}) diff --git a/packages/transport-tcp/test/max-connections-close.spec.ts b/packages/transport-tcp/test/max-connections-close.spec.ts new file mode 100644 index 0000000000..422a9f8b87 --- /dev/null +++ b/packages/transport-tcp/test/max-connections-close.spec.ts @@ -0,0 +1,121 @@ +import net from 'node:net' +import { promisify } from 'util' +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +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-tcp/test/max-connections.spec.ts b/packages/transport-tcp/test/max-connections.spec.ts new file mode 100644 index 0000000000..4e1634b01f --- /dev/null +++ b/packages/transport-tcp/test/max-connections.spec.ts @@ -0,0 +1,82 @@ +import net from 'node:net' +import { promisify } from 'node:util' +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' + +describe('maxConnections', () => { + const afterEachCallbacks: Array<() => Promise | any> = [] + afterEach(async () => { + await Promise.all(afterEachCallbacks.map(fn => fn())) + afterEachCallbacks.length = 0 + }) + + it('reject dial of connection above maxConnections', async () => { + const maxConnections = 2 + const socketCount = 4 + const port = 9900 + + const seenRemoteConnections = new Set() + const transport = tcp({ maxConnections })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = transport.createListener({ upgrader }) + // 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()) + }) + + const sockets: net.Socket[] = [] + + for (let i = 0; i < socketCount; i++) { + const socket = net.connect({ host: '127.0.0.1', port }) + sockets.push(socket) + + // 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)) + } + }) + + // Wait for connection so the order of sockets is stable, sockets expected to be alive are always [0,1] + await new Promise((resolve, reject) => { + socket.on('connect', () => { + resolve() + }) + socket.on('error', (err) => { + reject(err) + }) + }) + } + + // With server.maxConnections the TCP socket is created and the initial handshake is completed + // Then in the server handler NodeJS javascript code will call socket.emit('drop') if over the limit + // https://github.com/nodejs/node/blob/fddc701d3c0eb4520f2af570876cc987ae6b4ba2/lib/net.js#L1706 + + // Wait for some time for server to drop all sockets above limit + await promisify(setTimeout)(250) + + expect(seenRemoteConnections.size).equals(maxConnections, 'wrong serverConnections') + + for (let i = 0; i < socketCount; i++) { + const socket = sockets[i] + + if (i < maxConnections) { + // Assert socket connected + expect(socket.destroyed).equals(false, `socket ${i} under limit must not be destroyed`) + } else { + // Assert socket ended + expect(socket.destroyed).equals(true, `socket ${i} above limit must be destroyed`) + } + } + }) +}) diff --git a/packages/transport-tcp/test/socket-to-conn.spec.ts b/packages/transport-tcp/test/socket-to-conn.spec.ts new file mode 100644 index 0000000000..e4f8dbfd49 --- /dev/null +++ b/packages/transport-tcp/test/socket-to-conn.spec.ts @@ -0,0 +1,428 @@ +import { createServer, Socket, type Server, type ServerOpts, type SocketConstructorOpts } from 'net' +import os from 'os' +import { expect } from 'aegir/chai' +import defer from 'p-defer' +import { toMultiaddrConnection } from '../src/socket-to-conn.js' + +async function setup (opts?: { server?: ServerOpts, client?: SocketConstructorOpts }): Promise<{ server: Server, serverSocket: Socket, clientSocket: Socket }> { + const serverListening = defer() + + const server = createServer(opts?.server) + server.listen(0, () => { + serverListening.resolve() + }) + + await serverListening.promise + + const serverSocket = defer() + const clientSocket = defer() + + server.once('connection', (socket) => { + serverSocket.resolve(socket) + }) + + const address = server.address() + + if (address == null || typeof address === 'string') { + throw new Error('Wrong socket type') + } + + const client = new Socket(opts?.client) + client.once('connect', () => { + clientSocket.resolve(client) + }) + client.connect(address.port, address.address) + + return { + server, + serverSocket: await serverSocket.promise, + clientSocket: await clientSocket.promise + } +} + +describe('socket-to-conn', () => { + let server: Server + let clientSocket: Socket + let serverSocket: Socket + + afterEach(async () => { + if (serverSocket != null) { + serverSocket.destroy() + } + + if (clientSocket != null) { + clientSocket.destroy() + } + + if (server != null) { + server.close() + } + }) + + it('should destroy a socket that is closed by the client', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when client socket is closed + const clientClosed = defer() + + // promise that is resolved when client socket errors + const clientErrored = defer() + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('close', () => { + clientClosed.resolve(true) + }) + clientSocket.once('error', err => { + clientErrored.resolve(err) + }) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // close the client for writing + clientSocket.end() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket that is forcibly closed by the client', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // close the client for reading and writing immediately + clientSocket.destroy() + + // client closed the connection - error code is platform specific + if (os.platform() === 'linux') { + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_READ_TIMEOUT') + } else { + await expect(serverErrored.promise).to.eventually.have.property('code', 'ECONNRESET') + } + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket that is half-closed by the client', async () => { + ({ server, clientSocket, serverSocket } = await setup({ + client: { + allowHalfOpen: true + } + })) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // close the client for writing + clientSocket.end() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // remote stopped sending us data + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_READ_TIMEOUT') + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket after sinking', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + await inboundMaConn.sink([ + Uint8Array.from([0, 1, 2, 3]) + ]) + + // server socket should no longer be writable + expect(serverSocket.writable).to.be.false() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // remote didn't send us any data + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_READ_TIMEOUT') + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket when containing MultiaddrConnection is closed', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100, + socketCloseTimeout: 10 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + await inboundMaConn.close() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket by timeout when containing MultiaddrConnection is closed', async () => { + ({ server, clientSocket, serverSocket } = await setup({ + server: { + allowHalfOpen: true + } + })) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100, + socketCloseTimeout: 10 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + await inboundMaConn.close() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket by timeout when containing MultiaddrConnection is closed but remote keeps sending data', async () => { + ({ server, clientSocket, serverSocket } = await setup({ + server: { + allowHalfOpen: true + } + })) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 500, + socketCloseTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + setInterval(() => { + clientSocket.write(`some data ${Date.now()}`) + }, 10).unref() + + await inboundMaConn.close() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket by timeout when containing MultiaddrConnection is closed but closing remote times out', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + let maConnCloseError: Error | undefined + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100, + socketCloseTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // stop reading data + clientSocket.pause() + + // have to write enough data quickly enough to overwhelm the client + while (serverSocket.writableLength < 1024) { + serverSocket.write('goodbyeeeeeeeeeeeeee') + } + + await inboundMaConn.close().catch(err => { + // should throw this error + maConnCloseError = err + }) + + // server socket should no longer be writable + expect(serverSocket.writable).to.be.false() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // remote didn't read our data + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_CLOSE_TIMEOUT') + + // closing should have thrown + expect(maConnCloseError).to.have.property('code', 'ERR_SOCKET_CLOSE_TIMEOUT') + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) +}) diff --git a/packages/transport-tcp/tsconfig.json b/packages/transport-tcp/tsconfig.json new file mode 100644 index 0000000000..770ba81a1e --- /dev/null +++ b/packages/transport-tcp/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-transport" + }, + { + "path": "../interface-transport-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../utils" + } + ] +} diff --git a/packages/transport-webrtc/.aegir.js b/packages/transport-webrtc/.aegir.js new file mode 100644 index 0000000000..f19d27456f --- /dev/null +++ b/packages/transport-webrtc/.aegir.js @@ -0,0 +1,61 @@ + +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + config: { + platform: 'node' + }, + bundlesizeMax: '117KB' + }, + test: { + 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') + + // start a relay node for use in the tests + const relay = await createLibp2p({ + addresses: { + listen: [ + '/ip4/127.0.0.1/tcp/0/ws' + ] + }, + transports: [ + webSockets() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + services: { + relay: circuitRelayServer({ + reservations: { + maxReservations: Infinity + } + }), + identify: identifyService() + }, + connectionManager: { + minConnections: 0 + } + }) + + const multiaddrs = relay.getMultiaddrs().map(ma => ma.toString()) + + return { + relay, + env: { + RELAY_MULTIADDR: multiaddrs[0] + } + } + }, + after: async (_, before) => { + await before.relay.stop() + } + } +} diff --git a/packages/transport-webrtc/CHANGELOG.md b/packages/transport-webrtc/CHANGELOG.md new file mode 100644 index 0000000000..d4076ee6de --- /dev/null +++ b/packages/transport-webrtc/CHANGELOG.md @@ -0,0 +1,242 @@ +## [2.0.10](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.9...v2.0.10) (2023-06-12) + + +### Bug Fixes + +* 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)) + +## [2.0.9](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.8...v2.0.9) (2023-06-12) + + +### Dependencies + +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#169](https://github.com/libp2p/js-libp2p-webrtc/issues/169)) ([104cbf0](https://github.com/libp2p/js-libp2p-webrtc/commit/104cbf0e2009961656cda530925089dc126b19a8)) + +## [2.0.8](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.7...v2.0.8) (2023-06-12) + + +### Tests + +* add a test for large transfers ([#175](https://github.com/libp2p/js-libp2p-webrtc/issues/175)) ([0f60060](https://github.com/libp2p/js-libp2p-webrtc/commit/0f60060c9ceaf2bf2142df25f32174112edf6ec9)) + +## [2.0.7](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.6...v2.0.7) (2023-06-07) + + +### Tests + +* actually run firefox tests on firefox ([#176](https://github.com/libp2p/js-libp2p-webrtc/issues/176)) ([386a607](https://github.com/libp2p/js-libp2p-webrtc/commit/386a6071923e6cb1d89c51b73dada306b7cc243f)) + +## [2.0.6](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.5...v2.0.6) (2023-06-04) + + +### Documentation + +* update README.md example ([#178](https://github.com/libp2p/js-libp2p-webrtc/issues/178)) ([1264875](https://github.com/libp2p/js-libp2p-webrtc/commit/1264875ebd40b057e70aa47bebde45bfbe80facb)) + +## [2.0.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.4...v2.0.5) (2023-06-01) + + +### Bug Fixes + +* Update splitAddr function to correctly parse multiaddrs ([#174](https://github.com/libp2p/js-libp2p-webrtc/issues/174)) ([22a7029](https://github.com/libp2p/js-libp2p-webrtc/commit/22a7029caab7601cfc1f1d1051bc218ebe4dfce0)) + +## [2.0.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.3...v2.0.4) (2023-05-17) + + +### Bug Fixes + +* use abstract stream class from muxer interface module ([#165](https://github.com/libp2p/js-libp2p-webrtc/issues/165)) ([32f68de](https://github.com/libp2p/js-libp2p-webrtc/commit/32f68de455d2f0b136553aa41caf06adaf1f09d1)), closes [#164](https://github.com/libp2p/js-libp2p-webrtc/issues/164) + +## [2.0.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.2...v2.0.3) (2023-05-17) + + +### Bug Fixes + +* restrict message sizes to 16kb ([#147](https://github.com/libp2p/js-libp2p-webrtc/issues/147)) ([aca4422](https://github.com/libp2p/js-libp2p-webrtc/commit/aca4422f5d4b81576d8c3cc5531cef7b7491abd2)), closes [#144](https://github.com/libp2p/js-libp2p-webrtc/issues/144) [#158](https://github.com/libp2p/js-libp2p-webrtc/issues/158) + +## [2.0.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.1...v2.0.2) (2023-05-15) + + +### Bug Fixes + +* use transport manager getListeners to get listen addresses ([#166](https://github.com/libp2p/js-libp2p-webrtc/issues/166)) ([2e144f9](https://github.com/libp2p/js-libp2p-webrtc/commit/2e144f977a2025aa3adce1816d5f7d0dc3aaa477)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.0...v2.0.1) (2023-05-12) + + +### Bug Fixes + +* remove protobuf-ts and split code into two folders ([#162](https://github.com/libp2p/js-libp2p-webrtc/issues/162)) ([64723a7](https://github.com/libp2p/js-libp2p-webrtc/commit/64723a726302edcdc7ec958a759c3c587a184d69)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.2.0...v2.0.0) (2023-05-11) + + +### ⚠ BREAKING CHANGES + +* must be used with libp2p@0.45.x + +### Dependencies + +* update all libp2p deps for compat with libp2p@0.45.x ([#160](https://github.com/libp2p/js-libp2p-webrtc/issues/160)) ([b20875d](https://github.com/libp2p/js-libp2p-webrtc/commit/b20875d9f73e5cad05376db2d1228363dd1bce7d)) + +## [1.2.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.11...v1.2.0) (2023-05-09) + + +### Features + +* export metrics ([#71](https://github.com/libp2p/js-libp2p-webrtc/issues/71)) ([b3cb445](https://github.com/libp2p/js-libp2p-webrtc/commit/b3cb445e226d6d4ddba092cf961d6178d9a19ac1)) + +## [1.1.11](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.10...v1.1.11) (2023-05-06) + + +### Dependencies + +* upgrade transport interface to 4.0.1 ([#150](https://github.com/libp2p/js-libp2p-webrtc/issues/150)) ([dc61fa2](https://github.com/libp2p/js-libp2p-webrtc/commit/dc61fa27a2f53568b1f3b320971de166b5b243f9)) + +## [1.1.10](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.9...v1.1.10) (2023-05-03) + + +### Bug Fixes + +* Fetch local fingerprint from SDP ([#109](https://github.com/libp2p/js-libp2p-webrtc/issues/109)) ([3673d6c](https://github.com/libp2p/js-libp2p-webrtc/commit/3673d6c2637c21e488e684cdff4eedbb7f5b3692)) + +## [1.1.9](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.8...v1.1.9) (2023-04-26) + + +### Documentation + +* update import in README example ([#141](https://github.com/libp2p/js-libp2p-webrtc/issues/141)) ([42275df](https://github.com/libp2p/js-libp2p-webrtc/commit/42275df0727cd729006cbf3fae300fc428c9ca51)) + +## [1.1.8](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.7...v1.1.8) (2023-04-25) + + +### Bug Fixes + +* added peer connection state listener to emit closed events ([#134](https://github.com/libp2p/js-libp2p-webrtc/issues/134)) ([16e8503](https://github.com/libp2p/js-libp2p-webrtc/commit/16e85030e78ed9edb2ebecf81bac3ad33d622111)), closes [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) + +## [1.1.7](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.6...v1.1.7) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#135](https://github.com/libp2p/js-libp2p-webrtc/issues/135)) ([2fc8399](https://github.com/libp2p/js-libp2p-webrtc/commit/2fc839912a65c310ca7c8935d1901cc56849a21d)) + +## [1.1.6](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.5...v1.1.6) (2023-04-21) + + +### Bug Fixes + +* readme: Remove confusing section ([#122](https://github.com/libp2p/js-libp2p-webrtc/issues/122)) ([dc78154](https://github.com/libp2p/js-libp2p-webrtc/commit/dc781543b8175c6c40c6745029a4ba53587aef29)) + +## [1.1.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.4...v1.1.5) (2023-04-13) + + +### Dependencies + +* bump it-pipe from 2.0.5 to 3.0.1 ([#111](https://github.com/libp2p/js-libp2p-webrtc/issues/111)) ([7e593a3](https://github.com/libp2p/js-libp2p-webrtc/commit/7e593a34b44b7a2cf4758df2218b3ba9ebacfce9)) +* bump protons-runtime from 4.0.2 to 5.0.0 ([#117](https://github.com/libp2p/js-libp2p-webrtc/issues/117)) ([87cbb19](https://github.com/libp2p/js-libp2p-webrtc/commit/87cbb193e2a45642333498d9317ab17eb527d34d)) +* **dev:** bump protons from 6.1.3 to 7.0.2 ([#119](https://github.com/libp2p/js-libp2p-webrtc/issues/119)) ([fd20f4f](https://github.com/libp2p/js-libp2p-webrtc/commit/fd20f4f7a182a8edca5a511fe747885d24a60652)) + +## [1.1.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.3...v1.1.4) (2023-04-13) + + +### Dependencies + +* Update multiaddr to 12.1.1 and multiformats 11.0.2 ([#123](https://github.com/libp2p/js-libp2p-webrtc/issues/123)) ([e069784](https://github.com/libp2p/js-libp2p-webrtc/commit/e069784229f2495b3cebc2c2a85969f23f0e7acf)) + +## [1.1.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.2...v1.1.3) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#124](https://github.com/libp2p/js-libp2p-webrtc/issues/124)) ([4146761](https://github.com/libp2p/js-libp2p-webrtc/commit/4146761226118268d510c8834f894083ba5408d3)) + +## [1.1.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.1...v1.1.2) (2023-04-11) + + +### Bug Fixes + +* update multiaddr in webrtc connection to include webRTC ([#121](https://github.com/libp2p/js-libp2p-webrtc/issues/121)) ([6ea04db](https://github.com/libp2p/js-libp2p-webrtc/commit/6ea04db9800259963affcb3101ea542de79271c0)) + +## [1.1.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.0...v1.1.1) (2023-04-10) + + +### Dependencies + +* bump it-pb-stream from 2.0.4 to 3.2.1 ([#118](https://github.com/libp2p/js-libp2p-webrtc/issues/118)) ([7e2ac67](https://github.com/libp2p/js-libp2p-webrtc/commit/7e2ac6795ea096b3cf5dc2c4077f6f39821e0502)) + +## [1.1.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.5...v1.1.0) (2023-04-07) + + +### Features + +* Browser to Browser ([#90](https://github.com/libp2p/js-libp2p-webrtc/issues/90)) ([add5c46](https://github.com/libp2p/js-libp2p-webrtc/commit/add5c467a2d02058933e6e11751af0c850568eaf)) + +## [1.0.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.4...v1.0.5) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#103](https://github.com/libp2p/js-libp2p-webrtc/issues/103)) ([c78851f](https://github.com/libp2p/js-libp2p-webrtc/commit/c78851fe71f6a6ca79a146a7022e818378ea6721)) + + +### Trivial Changes + +* replace err-code with CodeError ([#82](https://github.com/libp2p/js-libp2p-webrtc/issues/82)) ([cfa6494](https://github.com/libp2p/js-libp2p-webrtc/commit/cfa6494c43c4edb977e70abe81a260bf0e03de73)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([f0ae5e7](https://github.com/libp2p/js-libp2p-webrtc/commit/f0ae5e78a0469bd1129d7b242e4fb41f0b2ed49e)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([4c8806c](https://github.com/libp2p/js-libp2p-webrtc/commit/4c8806c6d2a1a8eff48f0e2248203d48bd84c065)) + +## [1.0.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.3...v1.0.4) (2023-02-22) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.6 ([#94](https://github.com/libp2p/js-libp2p-webrtc/issues/94)) ([2ee8a5e](https://github.com/libp2p/js-libp2p-webrtc/commit/2ee8a5e4bb03377214ff3c12744c2e153a3f69b4)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7e0b1c0](https://github.com/libp2p/js-libp2p-webrtc/commit/7e0b1c00b28cae7249a506f06f18bf3537bf3476)) + +## [1.0.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.2...v1.0.3) (2023-01-30) + + +### Tests + +* add stream transition test ([#72](https://github.com/libp2p/js-libp2p-webrtc/issues/72)) ([27ec3da](https://github.com/libp2p/js-libp2p-webrtc/commit/27ec3da4ef66cf07c1452c6f987cb55d313c1a03)) + +## [1.0.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.1...v1.0.2) (2023-01-04) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#70](https://github.com/libp2p/js-libp2p-webrtc/issues/70)) ([7dafe5a](https://github.com/libp2p/js-libp2p-webrtc/commit/7dafe5a126ca0ce2b6d887f6a84fabe55e36229d)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.0...v1.0.1) (2023-01-03) + + +### Bug Fixes + +* remove uuid dependency ([#68](https://github.com/libp2p/js-libp2p-webrtc/issues/68)) ([fb14b88](https://github.com/libp2p/js-libp2p-webrtc/commit/fb14b880d1b1b278e1e826bb0d9939db358e6ccc)) + +## 1.0.0 (2022-12-13) + + +### Bug Fixes + +* update project config ([#65](https://github.com/libp2p/js-libp2p-webrtc/issues/65)) ([09c33cc](https://github.com/libp2p/js-libp2p-webrtc/commit/09c33ccfff97059eab001e46a662467dea670ce1)) + + +### Dependencies + +* update libp2p to release version ([dbd0237](https://github.com/libp2p/js-libp2p-webrtc/commit/dbd0237e9f8500ac13948e3a35d912df257968a4)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([43c70bc](https://github.com/libp2p/js-libp2p-webrtc/commit/43c70bcd3c63388ed44d76703ce9a32e51d9ef30)) + + +### Documentation + +* fix 'browser to server' build config ([#66](https://github.com/libp2p/js-libp2p-webrtc/issues/66)) ([b54132c](https://github.com/libp2p/js-libp2p-webrtc/commit/b54132cecac180f0577a1b7905f79b20207c3647)) diff --git a/packages/transport-webrtc/LICENSE b/packages/transport-webrtc/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-webrtc/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-webrtc/LICENSE-APACHE b/packages/transport-webrtc/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-webrtc/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-webrtc/LICENSE-MIT b/packages/transport-webrtc/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-webrtc/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/transport-webrtc/README.md b/packages/transport-webrtc/README.md new file mode 100644 index 0000000000..6b13043a49 --- /dev/null +++ b/packages/transport-webrtc/README.md @@ -0,0 +1,188 @@ +# @libp2p/webrtc + +[![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) + +> A libp2p transport using WebRTC connections + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Usage + +```js +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { multiaddr } from '@multiformats/multiaddr' +import first from 'it-first' +import { pipe } from 'it-pipe' +import { fromString, toString } from 'uint8arrays' +import { webRTC } from '@libp2p/webrtc' + +const node = await createLibp2p({ + transports: [webRTC()], + connectionEncryption: [noise()], +}); + +await node.start() + +const ma = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') +const stream = await node.dialProtocol(ma, ['/my-protocol/1.0.0']) +const message = `Hello js-libp2p-webrtc\n` +const response = await pipe([fromString(message)], stream, async (source) => await first(source)) +const responseDecoded = toString(response.slice(0, response.length)) +``` + +## Examples + +Examples can be found in the [examples folder](examples/README.md). + +## Interfaces + +### Transport + +![https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interface-transport](https://raw.githubusercontent.com/libp2p/js-libp2p-interfaces/master/packages/interface-transport/img/badge.png) + +Browsers can usually only `dial`, but `listen` is supported in the WebRTC +transport when paired with another listener like CircuitV2, where you listen on +a relayed connection. Take a look at [index.js](examples/browser-to-browser/index.js) for +an example. + +### Connection + +![https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interface-connection](https://raw.githubusercontent.com/libp2p/js-libp2p-interfaces/master/packages/interface-connection/img/badge.png) + +```js +interface MultiaddrConnection extends Duplex { + close: (err?: Error) => Promise + remoteAddr: Multiaddr + timeline: MultiaddrConnectionTimeline +} + +class WebRTCMultiaddrConnection implements MultiaddrConnection { } +``` + +## Development + +Contributions are welcome! The libp2p implementation in JavaScript is a work in progress. As such, there's a few things you can do right now to help out: + +- [Check out the existing issues](//github.com/little-bear-labs/js-libp2p-webrtc/issues). +- **Perform code reviews**. +- **Add tests**. There can never be enough tests. +- Go through the modules and **check out existing issues**. This is 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. + +Please be aware that all interactions related to libp2p are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +This module leans heavily on (Aegir)\[] for most of the `package.json` scripts. + +### Build + +The build script is a wrapper to `aegir build`. To build this package: + +```shell +npm run build +``` + +The build will be located in the `/dist` folder. + +### Protocol Buffers + +There is also `npm run generate:proto` script that uses protoc to populate the generated code directory `proto_ts` based on `*.proto` files in src. Don't forget to run this step before `build` any time you make a change to any of the `*.proto` files. + +### Test + +To run all tests: + +```shell +npm test +``` + +To run tests for Chrome only: + +```shell +npm run test:chrome +``` + +To run tests for Firefox only: + +```shell +npm run test:firefox +``` + +### Lint + +Aegir is also used to lint the code, which follows the [Standard](https://github.com/standard/standard) JS linter. +The VS Code plugin for this standard is located at . +To lint this repo: + +```shell +npm run lint +``` + +You can also auto-fix when applicable: + +```shell +npm run lint:fix +``` + +### Clean + +```shell +npm run clean +``` + +### Check Dependencies + +```shell +npm run deps-check +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-webrtc/examples/README.md b/packages/transport-webrtc/examples/README.md new file mode 100644 index 0000000000..8b78f83bbe --- /dev/null +++ b/packages/transport-webrtc/examples/README.md @@ -0,0 +1,4 @@ +# Examples + +* [Browser to Server Echo](browser-to-server/README.md): connect to a go-libp2p-webrtc server with a browser + diff --git a/packages/transport-webrtc/examples/browser-to-browser/README.md b/packages/transport-webrtc/examples/browser-to-browser/README.md new file mode 100644 index 0000000000..866513c1a6 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/README.md @@ -0,0 +1,61 @@ +# js-libp2p-webrtc Browser to Browser + +This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. + +## Build the `@libp2p/webrtc` package + +Build the `@libp2p/webrtc` package by calling `npm i && npm run build` in the repository root. + +## Running the Relay Server + +For browsers to communicate, we first need to run the LibP2P relay server: + +```shell +npm run relay +``` + +Copy one of the multiaddresses in the output. + +## Running the Example + +In a separate console tab, install dependencies and start the Vite server: + +```shell +npm i && npm run start +``` + +The browser window will automatically open. Let's call this `Browser A`. +Using the copied multiaddress from the Go or NodeJS relay server, paste it into the `Remote MultiAddress` input and click the `Connect` button. +`Browser A` is now connected to the relay server. +Copy the multiaddress located after the `Listening on` message. + +Now open a second browser with the url `http://localhost:5173/`. Let's call this `Browser B`. +Using the copied multiaddress from `Listening on` section in `Browser A`, paste it into the `Remote MultiAddress` input and click the `Connect` button. +`Browser B` is now connected to `Browser A`. +Copy the multiaddress located after the `Listening on` message. + +Using the copied multiaddress from `Listening on` section in `Browser B`, paste it into the `Remote MultiAddress` input in `Browser A` and click the `Connect` button. +`Browser A` is now connected to `Browser B`. + +The peers are now connected to each other. Enter a message and click the `Send` button in either/both browsers and see the echo'd messages. + +The output should look like: + +`Browser A` +```text +Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk' +Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC +Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9' +Sending message 'helloa' +Received message 'helloa' +Received message 'hellob' +``` + +`Browser B` +```text +Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC' +Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9 +Received message 'helloa' +Sending message 'hellob' +Received message 'hellob' +``` diff --git a/packages/transport-webrtc/examples/browser-to-browser/index.html b/packages/transport-webrtc/examples/browser-to-browser/index.html new file mode 100644 index 0000000000..8da321d9d0 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/index.html @@ -0,0 +1,49 @@ + + + + + + js-libp2p WebRTC + + + +
+
+ + + +
+
+ + + +
+
+

Active Connections:

+
    +
    +
    +

    Listening addresses:

    +
      +
      +
      +
      + + + diff --git a/packages/transport-webrtc/examples/browser-to-browser/index.js b/packages/transport-webrtc/examples/browser-to-browser/index.js new file mode 100644 index 0000000000..36edf09efd --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/index.js @@ -0,0 +1,133 @@ +import { multiaddr, protocols } from "@multiformats/multiaddr" +import { pipe } from "it-pipe" +import { fromString, toString } from "uint8arrays" +import { webRTC } from "@libp2p/webrtc" +import { webSockets } from "@libp2p/websockets" +import * as filters from "@libp2p/websockets/filters" +import { pushable } from "it-pushable" +import { mplex } from "@libp2p/mplex" +import { createLibp2p } from "libp2p" +import { circuitRelayTransport } from 'libp2p/circuit-relay' +import { noise } from "@chainsafe/libp2p-noise" +import { identifyService } from 'libp2p/identify' + +const WEBRTC_CODE = protocols('webrtc').code + +const output = document.getElementById("output") +const sendSection = document.getElementById("send-section") +const appendOutput = (line) => { + const div = document.createElement("div") + div.appendChild(document.createTextNode(line)) + output.append(div) +} +const clean = (line) => line.replaceAll("\n", "") +const sender = pushable() + +const node = await createLibp2p({ + addresses: { + listen: [ + '/webrtc' + ] + }, + transports: [ + webSockets({ + filter: filters.all, + }), + webRTC(), + circuitRelayTransport({ + discoverRelays: 1, + }), + ], + connectionEncryption: [noise()], + streamMuxers: [mplex()], + connectionGater: { + denyDialMultiaddr: () => { + // by default we refuse to dial local addresses from the browser since they + // are usually sent by remote peers broadcasting undialable multiaddrs but + // here we are explicitly connecting to a local node so do not deny dialing + // any discovered address + return false + } + }, + services: { + identify: identifyService() + } +}) + +await node.start() + +// handle the echo protocol +await node.handle("/echo/1.0.0", ({ stream }) => { + pipe( + stream, + async function* (source) { + for await (const buf of source) { + const incoming = toString(buf.subarray()) + appendOutput(`Received message '${clean(incoming)}'`) + yield buf + } + }, + stream + ) +}) + +function updateConnList() { + // Update connections list + const connListEls = node.getConnections() + .map((connection) => { + if (connection.remoteAddr.protoCodes().includes(WEBRTC_CODE)) { + sendSection.style.display = "block" + } + + const el = document.createElement("li") + el.textContent = connection.remoteAddr.toString() + return el + }) + document.getElementById("connections").replaceChildren(...connListEls) +} + +node.addEventListener("connection:open", (event) => { + updateConnList() +}) +node.addEventListener("connection:close", (event) => { + updateConnList() +}) + +node.addEventListener("self:peer:update", (event) => { + // Update multiaddrs list + const multiaddrs = node.getMultiaddrs() + .map((ma) => { + const el = document.createElement("li") + el.textContent = ma.toString() + return el + }) + document.getElementById("multiaddrs").replaceChildren(...multiaddrs) +}) + +const isWebrtc = (ma) => { + return ma.protoCodes().includes(WEBRTC_CODE) +} + +window.connect.onclick = async () => { + const ma = multiaddr(window.peer.value) + appendOutput(`Dialing '${ma}'`) + const connection = await node.dial(ma) + + if (isWebrtc(ma)) { + const outgoing_stream = await connection.newStream(["/echo/1.0.0"]) + pipe(sender, outgoing_stream, async (src) => { + for await (const buf of src) { + const response = toString(buf.subarray()) + appendOutput(`Received message '${clean(response)}'`) + } + }) + } + + appendOutput(`Connected to '${ma}'`) +} + +window.send.onclick = async () => { + const message = `${window.message.value}\n` + appendOutput(`Sending message '${clean(message)}'`) + sender.push(fromString(message)) +} diff --git a/packages/transport-webrtc/examples/browser-to-browser/package.json b/packages/transport-webrtc/examples/browser-to-browser/package.json new file mode 100644 index 0000000000..94368cbe64 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/package.json @@ -0,0 +1,27 @@ +{ + "name": "js-libp2p-webrtc-private-to-private", + "version": "1.0.0", + "description": "Connect a browser to another browser", + "type": "module", + "scripts": { + "start": "vite", + "build": "vite build", + "relay": "node relay.js", + "test:firefox": "npm run build && playwright test --browser=firefox tests", + "test:chrome": "npm run build && playwright test tests", + "test": "npm run build && test-browser-example tests" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.0", + "@libp2p/websockets": "^6.0.1", + "@libp2p/mplex": "^8.0.1", + "@libp2p/webrtc": "file:../../", + "@multiformats/multiaddr": "^12.0.0", + "it-pushable": "^3.1.0", + "libp2p": "^0.45.0", + "vite": "^4.2.1" + }, + "devDependencies": { + "test-ipfs-example": "^1.0.0" + } +} diff --git a/packages/transport-webrtc/examples/browser-to-browser/relay.js b/packages/transport-webrtc/examples/browser-to-browser/relay.js new file mode 100644 index 0000000000..15754c2bc9 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/relay.js @@ -0,0 +1,26 @@ +import { mplex } from "@libp2p/mplex" +import { createLibp2p } from "libp2p" +import { noise } from "@chainsafe/libp2p-noise" +import { circuitRelayServer } from 'libp2p/circuit-relay' +import { webSockets } from '@libp2p/websockets' +import * as filters from '@libp2p/websockets/filters' +import { identifyService } from 'libp2p/identify' + +const server = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0/ws'] + }, + transports: [ + webSockets({ + filter: filters.all + }), + ], + connectionEncryption: [noise()], + streamMuxers: [mplex()], + services: { + identify: identifyService(), + relay: circuitRelayServer() + } +}) + +console.log("p2p addr: ", server.getMultiaddrs().map((ma) => ma.toString())) diff --git a/packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js b/packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js new file mode 100644 index 0000000000..703ae688cc --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js @@ -0,0 +1,129 @@ +/* eslint-disable no-console */ +import { setup, expect } from 'test-ipfs-example/browser' +import { createLibp2p } from 'libp2p' +import { circuitRelayServer } from 'libp2p/circuit-relay' +import { webSockets } from '@libp2p/websockets' +import * as filters from '@libp2p/websockets/filters' +import { mplex } from '@libp2p/mplex' +import { noise } from '@chainsafe/libp2p-noise' +import { identifyService } from 'libp2p/identify' + +// Setup +const test = setup() + +// DOM +const connectBtn = '#connect' +const connectAddr = '#peer' +const messageInput = '#message' +const sendBtn = '#send' +const output = '#output' +const listeningAddresses = '#multiaddrs' + +let url + +// we spawn a js libp2p relay +async function spawnRelay() { + const relayNode = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0/ws'] + }, + transports: [ + webSockets({ + filter: filters.all + }), + ], + connectionEncryption: [noise()], + streamMuxers: [mplex()], + services: { + identify: identifyService(), + relay: circuitRelayServer() + } + }) + + const relayNodeAddr = relayNode.getMultiaddrs()[0].toString() + + return { relayNode, relayNodeAddr } +} + +test.describe('browser to browser example:', () => { + let relayNode + let relayNodeAddr + + // eslint-disable-next-line no-empty-pattern + test.beforeAll(async ({ servers }, testInfo) => { + testInfo.setTimeout(5 * 60_000) + const r = await spawnRelay() + relayNode = r.relayNode + relayNodeAddr = r.relayNodeAddr + console.log('Server addr:', relayNodeAddr) + url = servers[0].url + }, {}) + + test.afterAll(() => { + relayNode.stop() + }) + + test.beforeEach(async ({ page }) => { + await page.goto(url) + }) + + test('should connect to a relay node', async ({ page: pageA, context }) => { + // load second page + const pageB = await context.newPage() + await pageB.goto(url) + + // connect both pages to the relay + const relayedAddressA = await dialRelay(pageA, relayNodeAddr) + const relayedAddressB = await dialRelay(pageB, relayNodeAddr) + + // dial first page from second page over relay + await dialPeerOverRelay(pageA, relayedAddressB) + await dialPeerOverRelay(pageB, relayedAddressA) + + // stop the relay + await relayNode.stop() + + await echoMessagePeer(pageB, 'hello B') + + await echoMessagePeer(pageA, 'hello A') + }) +}) + +async function echoMessagePeer (page, message) { + // send the message to the peer over webRTC + await page.fill(messageInput, message) + await page.click(sendBtn) + + // check the message was echoed back + const outputLocator = page.locator(output) + await expect(outputLocator).toContainText(`Sending message '${message}'`) + await expect(outputLocator).toContainText(`Received message '${message}'`) +} + +async function dialRelay (page, address) { + // add the go libp2p multiaddress to the input field and submit + await page.fill(connectAddr, address) + await page.click(connectBtn) + + const outputLocator = page.locator(output) + await expect(outputLocator).toContainText(`Dialing '${address}'`) + await expect(outputLocator).toContainText(`Connected to '${address}'`) + + const multiaddrsLocator = page.locator(listeningAddresses) + await expect(multiaddrsLocator).toHaveText(/webrtc/) + + const multiaddrs = await page.textContent(listeningAddresses) + const addr = multiaddrs.split(address).filter(str => str.includes('webrtc')).pop() + + return address + addr +} + +async function dialPeerOverRelay (page, address) { + // add the go libp2p multiaddr to the input field and submit + await page.fill(connectAddr, address) + await page.click(connectBtn) + + const outputLocator = page.locator(output) + await expect(outputLocator).toContainText(`Dialing '${address}'`) + await expect(outputLocator).toContainText(`Connected to '${address}'`) +} diff --git a/packages/transport-webrtc/examples/browser-to-browser/vite.config.js b/packages/transport-webrtc/examples/browser-to-browser/vite.config.js new file mode 100644 index 0000000000..353f32b6ef --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/vite.config.js @@ -0,0 +1,11 @@ +export default { + build: { + target: 'es2022' + }, + optimizeDeps: { + esbuildOptions: { target: 'es2022', supported: { bigint: true } } + }, + server: { + open: true + } +} \ No newline at end of file diff --git a/packages/transport-webrtc/examples/browser-to-server/README.md b/packages/transport-webrtc/examples/browser-to-server/README.md new file mode 100644 index 0000000000..fb3d997503 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/README.md @@ -0,0 +1,34 @@ +# js-libp2p-webrtc Browser to Server + +This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. + +## Running the Go Server + +To run the Go LibP2P WebRTC server: + +```shell +npm run go-libp2p-server +``` + +Copy the multiaddress in the output. + +## Running the Example + +In a separate console tab, install dependencies and start the Vite server: + +```shell +npm i && npm run start +``` + +The browser window will automatically open. +Using the copied multiaddress from the Go server, paste it into the `Server MultiAddress` input and click the `Connect` button. +Once the peer is connected, click the message section will appear. Enter a message and click the `Send` button. + +The output should look like: + +```text +Dialing /ip4/10.0.1.5/udp/54375/webrtc/certhash/uEiADy8JubdWrAzseyzfXFyCpdRN02eWZg86tjCrTCA5dbQ/p2p/12D3KooWEG7N4bnZfFBNZE7WG6xm2P4Sr6sonMwyD4HCAqApEthb +Peer connected '/ip4/10.0.1.5/udp/54375/webrtc/certhash/uEiADy8JubdWrAzseyzfXFyCpdRN02eWZg86tjCrTCA5dbQ/p2p/12D3KooWEG7N4bnZfFBNZE7WG6xm2P4Sr6sonMwyD4HCAqApEthb' +Sending message 'hello' +Received message 'hello' +``` \ No newline at end of file diff --git a/packages/transport-webrtc/examples/browser-to-server/index.html b/packages/transport-webrtc/examples/browser-to-server/index.html new file mode 100644 index 0000000000..24ff11f5bd --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/index.html @@ -0,0 +1,41 @@ + + + + + + js-libp2p WebRTC + + + +
      +
      + + + +
      +
      + + + +
      +
      +
      + + + diff --git a/packages/transport-webrtc/examples/browser-to-server/index.js b/packages/transport-webrtc/examples/browser-to-server/index.js new file mode 100644 index 0000000000..5b9c14d03c --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/index.js @@ -0,0 +1,62 @@ +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { multiaddr } from '@multiformats/multiaddr' +import { pipe } from "it-pipe"; +import { fromString, toString } from "uint8arrays"; +import { webRTCDirect } from '@libp2p/webrtc' +import { pushable } from 'it-pushable'; + +let stream; +const output = document.getElementById('output') +const sendSection = document.getElementById('send-section') +const appendOutput = (line) => { + const div = document.createElement("div") + div.appendChild(document.createTextNode(line)) + output.append(div) +} +const clean = (line) => line.replaceAll('\n', '') +const sender = pushable() + +const node = await createLibp2p({ + transports: [webRTCDirect()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: () => { + // by default we refuse to dial local addresses from the browser since they + // are usually sent by remote peers broadcasting undialable multiaddrs but + // here we are explicitly connecting to a local node so do not deny dialing + // any discovered address + return false + } + } +}); + +await node.start() + +node.addEventListener('peer:connect', (connection) => { + appendOutput(`Peer connected '${node.getConnections().map(c => c.remoteAddr.toString())}'`) + sendSection.style.display = 'block' +}) + +window.connect.onclick = async () => { + // TODO!!(ckousik): hack until webrtc is renamed in Go. Remove once + // complete + let candidateMa = window.peer.value + candidateMa = candidateMa.replace(/\/webrtc\/certhash/, "/webrtc-direct/certhash") + const ma = multiaddr(candidateMa) + + appendOutput(`Dialing '${ma}'`) + stream = await node.dialProtocol(ma, ['/echo/1.0.0']) + pipe(sender, stream, async (src) => { + for await(const buf of src) { + const response = toString(buf.subarray()) + appendOutput(`Received message '${clean(response)}'`) + } + }) +} + +window.send.onclick = async () => { + const message = `${window.message.value}\n` + appendOutput(`Sending message '${clean(message)}'`) + sender.push(fromString(message)) +} diff --git a/packages/transport-webrtc/examples/browser-to-server/package.json b/packages/transport-webrtc/examples/browser-to-server/package.json new file mode 100644 index 0000000000..22e88e114e --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/package.json @@ -0,0 +1,25 @@ +{ + "name": "js-libp2p-webrtc-browser-to-server", + "version": "1.0.0", + "description": "Connect a browser to a server", + "type": "module", + "scripts": { + "start": "vite", + "build": "vite build", + "go-libp2p-server": "cd ../go-libp2p-server && go run ./main.go", + "test:chrome": "npm run build && playwright test tests", + "test:firefox": "npm run build && playwright test --browser firefox tests", + "test": "npm run build && test-browser-example tests" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.0", + "@libp2p/webrtc": "file:../../", + "@multiformats/multiaddr": "^12.0.0", + "it-pushable": "^3.1.0", + "libp2p": "^0.45.0", + "vite": "^4.2.1" + }, + "devDependencies": { + "test-ipfs-example": "^1.0.0" + } +} diff --git a/packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js b/packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js new file mode 100644 index 0000000000..dcdd25d348 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js @@ -0,0 +1,94 @@ +/* eslint-disable no-console */ +import { setup, expect } from 'test-ipfs-example/browser' +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' + +// Setup +const test = setup() + +async function spawnGoLibp2p() { + if (!existsSync('../../examples/go-libp2p-server/go-libp2p-server')) { + await new Promise((resolve, reject) => { + exec('go build', + { cwd: '../../examples/go-libp2p-server' }, + (error, stdout, stderr) => { + if (error) { + throw (`exec error: ${error}`) + } + resolve() + }) + }) + } + + const server = spawn('./go-libp2p-server', [], { cwd: '../../examples/go-libp2p-server', killSignal: 'SIGINT' }) + server.stderr.on('data', (data) => { + console.log(`stderr: ${data}`, typeof data) + }) + const serverAddr = await (new Promise(resolve => { + server.stdout.on('data', (data) => { + console.log(`stdout: ${data}`, typeof data) + const addr = String(data).match(/p2p addr: ([^\s]*)/) + if (addr !== null && addr.length > 0) { + resolve(addr[1]) + } + }) + })) + return { server, serverAddr } +} + +test.describe('bundle ipfs with parceljs:', () => { + // DOM + const connectBtn = '#connect' + const connectAddr = '#peer' + const messageInput = '#message' + const sendBtn = '#send' + const output = '#output' + + let server + let serverAddr + + // eslint-disable-next-line no-empty-pattern + test.beforeAll(async ({ }, testInfo) => { + testInfo.setTimeout(5 * 60_000) + const s = await spawnGoLibp2p() + server = s.server + serverAddr = s.serverAddr + console.log('Server addr:', serverAddr) + }, {}) + + test.afterAll(() => { + server.kill('SIGINT') + }) + + test.beforeEach(async ({ servers, page }) => { + await page.goto(servers[0].url) + }) + + test('should connect to a go-libp2p node over webrtc', async ({ page }) => { + const message = 'hello' + + // add the go libp2p multiaddress to the input field and submit + await page.fill(connectAddr, serverAddr) + await page.click(connectBtn) + + // send the relay message to the go libp2p server + await page.fill(messageInput, message) + await page.click(sendBtn) + + await page.waitForSelector('#output:has(div)') + + // Expected output: + // + // Dialing '${serverAddr}' + // Peer connected '${serverAddr}' + // Sending message '${message}' + // Received message '${message}' + const connections = await page.textContent(output) + + expect(connections).toContain(`Dialing '${serverAddr}'`) + expect(connections).toContain(`Peer connected '${serverAddr}'`) + + expect(connections).toContain(`Sending message '${message}'`) + expect(connections).toContain(`Received message '${message}'`) + }) +}) diff --git a/packages/transport-webrtc/examples/browser-to-server/vite.config.js b/packages/transport-webrtc/examples/browser-to-server/vite.config.js new file mode 100644 index 0000000000..9b2e2a7b1f --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/vite.config.js @@ -0,0 +1,8 @@ +export default { + build: { + target: 'es2022' + }, + optimizeDeps: { + esbuildOptions: { target: 'es2022', supported: { bigint: true } } + }, +} \ No newline at end of file diff --git a/packages/transport-webrtc/examples/go-libp2p-server/.gitignore b/packages/transport-webrtc/examples/go-libp2p-server/.gitignore new file mode 100644 index 0000000000..baadac2c80 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/.gitignore @@ -0,0 +1 @@ +go-libp2p-server \ No newline at end of file diff --git a/packages/transport-webrtc/examples/go-libp2p-server/go.mod b/packages/transport-webrtc/examples/go-libp2p-server/go.mod new file mode 100644 index 0000000000..0742d25556 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/go.mod @@ -0,0 +1,117 @@ +module github.com/libp2p/js-libp2p-webrtc/examples/go-libp2p-server + +go 1.18 + +// TODO: Remove this once webrtc is merged into Go libp2p +replace github.com/libp2p/go-libp2p => github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3 + +require github.com/libp2p/go-libp2p v0.26.3 + +require ( + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.2 // indirect + github.com/flynn/noise v1.0.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/huin/goupnp v1.1.0 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-nat v0.1.0 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect + github.com/libp2p/go-reuseport v0.2.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.52 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr v0.9.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.8.1 // indirect + github.com/multiformats/go-multihash v0.2.1 // indirect + github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/onsi/ginkgo/v2 v2.9.1 // indirect + github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.5 // indirect + github.com/pion/dtls/v2 v2.1.5 // indirect + github.com/pion/ice/v2 v2.2.13 // indirect + github.com/pion/interceptor v0.1.12 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns v0.0.5 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.10 // indirect + github.com/pion/rtp v1.7.13 // indirect + github.com/pion/sctp v1.8.6 // indirect + github.com/pion/sdp/v3 v3.0.6 // indirect + github.com/pion/srtp/v2 v2.0.11 // indirect + github.com/pion/stun v0.4.0 // indirect + github.com/pion/transport v0.14.1 // indirect + github.com/pion/transport/v2 v2.0.0 // indirect + github.com/pion/turn/v2 v2.0.9 // indirect + github.com/pion/udp v0.1.1 // indirect + github.com/pion/webrtc/v3 v3.1.51 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.2.1 // indirect + github.com/quic-go/qtls-go1-20 v0.1.1 // indirect + github.com/quic-go/quic-go v0.33.0 // indirect + github.com/quic-go/webtransport-go v0.5.2 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/dig v1.16.1 // indirect + go.uber.org/fx v1.19.2 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + lukechampine.com/blake3 v1.1.7 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/packages/transport-webrtc/examples/go-libp2p-server/go.sum b/packages/transport-webrtc/examples/go-libp2p-server/go.sum new file mode 100644 index 0000000000..1faf17ae57 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/go.sum @@ -0,0 +1,1174 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= +github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221203041831-ce31453925ec/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d h1:um9/pc7tKMINFfP1eE7Wv6PRGXlcCSJkVajF7KJw3uQ= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= +github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goupnp v1.1.0 h1:gEe0Dp/lZmPZiDFzJJaOfUpOvv2MKUkoBX8lDrn9vKU= +github.com/huin/goupnp v1.1.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/ipfs/go-cid v0.2.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ds-badger v0.3.0/go.mod h1:1ke6mXNqeV8K3y5Ak2bAA0osoTfmxUdupVCGm4QUIek= +github.com/ipfs/go-ds-leveldb v0.5.0/go.mod h1:d3XG9RUDzQ6V4SHi8+Xgj9j1XuEk1z82lquxrVbml/Q= +github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= +github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= +github.com/ipfs/go-log/v2 v2.5.0/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= +github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= +github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3 h1:PbtmtrIDY1Us9qeGJdHO1nfp0Jik1KZIP64/KYK43YI= +github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3/go.mod h1:PwdLfPiWNhYkb96Wqc2uDFd/+0SGE07/IvjAoFiYG70= +github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= +github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-mplex v0.7.0/go.mod h1:rW8ThnRcYWft/Jb2jeORBmPd6xuG3dGxWN/W168L9EU= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= +github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= +github.com/libp2p/go-netroute v0.1.2/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= +github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= +github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= +github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= +github.com/libp2p/go-sockaddr v0.0.2/go.mod h1:syPvOmNs24S3dFVGJA1/mrqdeijPxLV2Le3BRLKd68k= +github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ= +github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c= +github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base32 v0.0.4/go.mod h1:jNLFzjPZtp3aIARHbJRZIaPuspdH0J6q39uUM5pnABM= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= +github.com/multiformats/go-multiaddr v0.6.0/go.mod h1:F4IpaKZuPP360tOMn2Tpyu0At8w23aRyVqeK0DbFeGM= +github.com/multiformats/go-multiaddr v0.9.0 h1:3h4V1LHIk5w4hJHekMKWALPXErDfz/sggzwC/NcqbDQ= +github.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc46ElrvzhYnoJOmTT0= +github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= +github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.5.0/go.mod h1:DiY2HFaEp5EhEXb/iYzVAunmyX/aSFMxq2KMKfWEues= +github.com/multiformats/go-multicodec v0.7.0/go.mod h1:GUC8upxSBE4oG+q3kWZRw/+6yC1BqO550bjhWsJbZlw= +github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= +github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg= +github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= +github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= +github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.5.1/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3 h1:5VwIwnBY3vbBDOJrNtA4rVdiTZCsq9B5F12pvy1Drmk= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= +github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= +github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c= +github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= +github.com/pion/ice/v2 v2.2.13 h1:NvLtzwcyob6wXgFqLmVQbGB3s9zzWmOegNMKYig5l9M= +github.com/pion/ice/v2 v2.2.13/go.mod h1:eFO4/1zCI+a3OFVt7l7kP+5jWCuZo8FwU2UwEa3+164= +github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= +github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8= +github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= +github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= +github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= +github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= +github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI= +github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= +github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v2 v2.0.11 h1:6cEEgT1oCLWgE+BynbfaSMAxtsqU0M096x9dNH6olY0= +github.com/pion/srtp/v2 v2.0.11/go.mod h1:vzHprzbuVoYJ9NfaRMycnFrkHcLSaLVuBZDOtFQNZjY= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk= +github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= +github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= +github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= +github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/turn/v2 v2.0.9 h1:jcDPw0Vfd5I4iTc7s0Upfc2aMnyu2lgJ9vV0SUrNC1o= +github.com/pion/turn/v2 v2.0.9/go.mod h1:DQlwUwx7hL8Xya6TTAabbd9DdKXTNR96Xf5g5Qqso/M= +github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/webrtc/v3 v3.1.51 h1:uU9vHdY63O3uRFJiDskH0qFJ+219bAH28qOt5csSWcM= +github.com/pion/webrtc/v3 v3.1.51/go.mod h1:sbRNshM9l0zRDQgZRP9K5RTzlsdBmqmyO8KbxngG8jQ= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= +github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A= +github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk= +github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= +github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= +github.com/quic-go/webtransport-go v0.5.2 h1:GA6Bl6oZY+g/flt00Pnu0XtivSD8vukOu3lYhJjnGEk= +github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= +go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= +go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= +go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= +go.uber.org/fx v1.19.2/go.mod h1:43G1VcqSzbIv77y00p1DRAsyZS8WdzuYdhZXmEUkMyQ= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= +lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/packages/transport-webrtc/examples/go-libp2p-server/main.go b/packages/transport-webrtc/examples/go-libp2p-server/main.go new file mode 100644 index 0000000000..a5f99e6f25 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "os" + "os/signal" + "syscall" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + webrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" +) + +var listenerIp = net.IPv4(127, 0, 0, 1) + +func init() { + ifaces, err := net.Interfaces() + if err != nil { + return + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + return + } + for _, addr := range addrs { + // bind to private non-loopback ip + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.IsPrivate() { + if ipnet.IP.To4() != nil { + listenerIp = ipnet.IP.To4() + return + } + } + } + } +} + +func echoHandler(stream network.Stream) { + for { + reader := bufio.NewReader(stream) + str, err := reader.ReadString('\n') + log.Printf("err: %s", err) + if err != nil { + return + } + log.Printf("echo: %s", str) + _, err = stream.Write([]byte(str)) + if err != nil { + log.Printf("err: %v", err) + return + } + } +} + +func main() { + host := createHost() + host.SetStreamHandler("/echo/1.0.0", echoHandler) + defer host.Close() + remoteInfo := peer.AddrInfo{ + ID: host.ID(), + Addrs: host.Network().ListenAddresses(), + } + + remoteAddrs, _ := peer.AddrInfoToP2pAddrs(&remoteInfo) + fmt.Println("p2p addr: ", remoteAddrs[0]) + + fmt.Println("press Ctrl+C to quit") + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) + <-ch +} + +func createHost() host.Host { + h, err := libp2p.New( + libp2p.Transport(webrtc.New), + libp2p.ListenAddrStrings( + fmt.Sprintf("/ip4/%s/udp/0/webrtc-direct", listenerIp), + ), + libp2p.DisableRelay(), + libp2p.Ping(true), + ) + if err != nil { + panic(err) + } + + return h +} diff --git a/packages/transport-webrtc/package.json b/packages/transport-webrtc/package.json new file mode 100644 index 0000000000..3a5543d2ad --- /dev/null +++ b/packages/transport-webrtc/package.json @@ -0,0 +1,95 @@ +{ + "name": "@libp2p/webrtc", + "version": "2.0.10", + "description": "A libp2p transport using WebRTC connections", + "author": "", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-webrtc#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "generate": "protons src/private-to-private/pb/message.proto src/pb/message.proto", + "build": "aegir build", + "test": "aegir test -t browser", + "test:chrome": "aegir test -t browser --cov", + "test:firefox": "aegir test -t browser -- --browser firefox", + "lint": "aegir lint", + "lint:fix": "aegir lint --fix", + "clean": "aegir clean", + "dep-check": "aegir dep-check -i protons" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.1", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-registrar": "^2.0.0", + "@libp2p/interface-stream-muxer": "^4.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", + "abortable-iterator": "^5.0.1", + "detect-browser": "^5.3.0", + "it-length-prefixed": "^9.0.1", + "it-pb-stream": "^4.0.1", + "it-pipe": "^3.0.1", + "it-pushable": "^3.1.3", + "it-stream-types": "^2.0.1", + "it-to-buffer": "^4.0.2", + "multiformats": "^11.0.2", + "multihashes": "^4.0.3", + "p-defer": "^4.0.0", + "p-event": "^6.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@chainsafe/libp2p-yamux": "^4.0.1", + "@libp2p/interface-libp2p": "^3.0.0", + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@libp2p/websockets": "^6.0.0", + "@types/sinon": "^10.0.15", + "aegir": "^39.0.10", + "delay": "^6.0.0", + "it-length": "^3.0.2", + "it-map": "^3.0.3", + "it-pair": "^2.0.6", + "libp2p": "^0.45.0", + "protons": "^7.0.2", + "sinon": "^15.1.0", + "sinon-ts": "^1.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-webrtc/src/error.ts b/packages/transport-webrtc/src/error.ts new file mode 100644 index 0000000000..360b4e3d73 --- /dev/null +++ b/packages/transport-webrtc/src/error.ts @@ -0,0 +1,122 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import type { Direction } from '@libp2p/interface-connection' + +export enum codes { + ERR_ALREADY_ABORTED = 'ERR_ALREADY_ABORTED', + ERR_DATA_CHANNEL = 'ERR_DATA_CHANNEL', + ERR_CONNECTION_CLOSED = 'ERR_CONNECTION_CLOSED', + ERR_HASH_NOT_SUPPORTED = 'ERR_HASH_NOT_SUPPORTED', + ERR_INVALID_MULTIADDR = 'ERR_INVALID_MULTIADDR', + ERR_INVALID_FINGERPRINT = 'ERR_INVALID_FINGERPRINT', + ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', + ERR_NOT_IMPLEMENTED = 'ERR_NOT_IMPLEMENTED', + ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS', + ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS', +} + +export class WebRTCTransportError extends CodeError { + constructor (msg: string, code?: string) { + super(`WebRTC transport error: ${msg}`, code ?? '') + this.name = 'WebRTCTransportError' + } +} + +export class ConnectionClosedError extends WebRTCTransportError { + constructor (state: RTCPeerConnectionState, msg: string) { + super(`peerconnection moved to state: ${state}: ${msg}`, codes.ERR_CONNECTION_CLOSED) + this.name = 'WebRTC/ConnectionClosed' + } +} + +export function connectionClosedError (state: RTCPeerConnectionState, msg: string): ConnectionClosedError { + return new ConnectionClosedError(state, msg) +} + +export class DataChannelError extends WebRTCTransportError { + constructor (streamLabel: string, msg: string) { + super(`[stream: ${streamLabel}] data channel error: ${msg}`, codes.ERR_DATA_CHANNEL) + this.name = 'WebRTC/DataChannelError' + } +} + +export function dataChannelError (streamLabel: string, msg: string): DataChannelError { + return new DataChannelError(streamLabel, msg) +} + +export class InappropriateMultiaddrError extends WebRTCTransportError { + constructor (msg: string) { + super(`There was a problem with the Multiaddr which was passed in: ${msg}`, codes.ERR_INVALID_MULTIADDR) + this.name = 'WebRTC/InappropriateMultiaddrError' + } +} + +export function inappropriateMultiaddr (msg: string): InappropriateMultiaddrError { + return new InappropriateMultiaddrError(msg) +} + +export class InvalidArgumentError extends WebRTCTransportError { + constructor (msg: string) { + super(`There was a problem with a provided argument: ${msg}`, codes.ERR_INVALID_PARAMETERS) + this.name = 'WebRTC/InvalidArgumentError' + } +} + +export function invalidArgument (msg: string): InvalidArgumentError { + return new InvalidArgumentError(msg) +} + +export class InvalidFingerprintError extends WebRTCTransportError { + constructor (fingerprint: string, source: string) { + super(`Invalid fingerprint "${fingerprint}" within ${source}`, codes.ERR_INVALID_FINGERPRINT) + this.name = 'WebRTC/InvalidFingerprintError' + } +} + +export function invalidFingerprint (fingerprint: string, source: string): InvalidFingerprintError { + return new InvalidFingerprintError(fingerprint, source) +} + +export class OperationAbortedError extends WebRTCTransportError { + constructor (context: string, abortReason: string) { + super(`Signalled to abort because (${abortReason}}) ${context}`, codes.ERR_ALREADY_ABORTED) + this.name = 'WebRTC/OperationAbortedError' + } +} + +export function operationAborted (context: string, reason: string): OperationAbortedError { + return new OperationAbortedError(context, reason) +} + +export class OverStreamLimitError extends WebRTCTransportError { + constructor (msg: string) { + const code = msg.startsWith('inbound') ? codes.ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS : codes.ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS + super(msg, code) + this.name = 'WebRTC/OverStreamLimitError' + } +} + +export function overStreamLimit (dir: Direction, proto: string): OverStreamLimitError { + return new OverStreamLimitError(`${dir} stream limit reached for protocol - ${proto}`) +} + +export class UnimplementedError extends WebRTCTransportError { + constructor (methodName: string) { + super(`A method (${methodName}) was called though it has been intentionally left unimplemented.`, codes.ERR_NOT_IMPLEMENTED) + this.name = 'WebRTC/UnimplementedError' + } +} + +export function unimplemented (methodName: string): UnimplementedError { + return new UnimplementedError(methodName) +} + +export class UnsupportedHashAlgorithmError extends WebRTCTransportError { + constructor (algo: string) { + super(`unsupported hash algorithm: ${algo}`, codes.ERR_HASH_NOT_SUPPORTED) + this.name = 'WebRTC/UnsupportedHashAlgorithmError' + } +} + +export function unsupportedHashAlgorithm (algorithm: string): UnsupportedHashAlgorithmError { + return new UnsupportedHashAlgorithmError(algorithm) +} diff --git a/packages/transport-webrtc/src/index.ts b/packages/transport-webrtc/src/index.ts new file mode 100644 index 0000000000..b35a16ced4 --- /dev/null +++ b/packages/transport-webrtc/src/index.ts @@ -0,0 +1,31 @@ +import { WebRTCTransport } from './private-to-private/transport.js' +import { WebRTCDirectTransport, type WebRTCTransportDirectInit, type WebRTCDirectTransportComponents } from './private-to-public/transport.js' +import type { WebRTCTransportComponents, WebRTCTransportInit } from './private-to-private/transport.js' +import type { Transport } from '@libp2p/interface-transport' + +/** + * @param {WebRTCTransportDirectInit} init - WebRTC direct transport configuration + * @param init.dataChannel - DataChannel configurations + * @param {number} init.dataChannel.maxMessageSize - Max message size that can be sent through the DataChannel. Larger messages will be chunked into smaller messages below this size (default 16kb) + * @param {number} init.dataChannel.maxBufferedAmount - Max buffered amount a DataChannel can have (default 16mb) + * @param {number} init.dataChannel.bufferedAmountLowEventTimeout - If max buffered amount is reached, this is the max time that is waited before the buffer is cleared (default 30 seconds) + * @returns + */ +function webRTCDirect (init?: WebRTCTransportDirectInit): (components: WebRTCDirectTransportComponents) => Transport { + return (components: WebRTCDirectTransportComponents) => new WebRTCDirectTransport(components, init) +} + +/** + * @param {WebRTCTransportInit} init - WebRTC transport configuration + * @param {RTCConfiguration} init.rtcConfiguration - RTCConfiguration + * @param init.dataChannel - DataChannel configurations + * @param {number} init.dataChannel.maxMessageSize - Max message size that can be sent through the DataChannel. Larger messages will be chunked into smaller messages below this size (default 16kb) + * @param {number} init.dataChannel.maxBufferedAmount - Max buffered amount a DataChannel can have (default 16mb) + * @param {number} init.dataChannel.bufferedAmountLowEventTimeout - If max buffered amount is reached, this is the max time that is waited before the buffer is cleared (default 30 seconds) + * @returns + */ +function webRTC (init?: WebRTCTransportInit): (components: WebRTCTransportComponents) => Transport { + return (components: WebRTCTransportComponents) => new WebRTCTransport(components, init) +} + +export { webRTC, webRTCDirect } diff --git a/packages/transport-webrtc/src/maconn.ts b/packages/transport-webrtc/src/maconn.ts new file mode 100644 index 0000000000..3ce4e3c458 --- /dev/null +++ b/packages/transport-webrtc/src/maconn.ts @@ -0,0 +1,85 @@ +import { logger } from '@libp2p/logger' +import { nopSink, nopSource } from './util.js' +import type { MultiaddrConnection, MultiaddrConnectionTimeline } from '@libp2p/interface-connection' +import type { CounterGroup } from '@libp2p/interface-metrics' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Source, Sink } from 'it-stream-types' + +const log = logger('libp2p:webrtc:connection') + +interface WebRTCMultiaddrConnectionInit { + /** + * WebRTC Peer Connection + */ + peerConnection: RTCPeerConnection + + /** + * The multiaddr address used to communicate with the remote peer + */ + remoteAddr: Multiaddr + + /** + * Holds the relevant events timestamps of the connection + */ + timeline: MultiaddrConnectionTimeline + + /** + * Optional metrics counter group for this connection + */ + metrics?: CounterGroup +} + +export class WebRTCMultiaddrConnection implements MultiaddrConnection { + /** + * WebRTC Peer Connection + */ + readonly peerConnection: RTCPeerConnection + + /** + * The multiaddr address used to communicate with the remote peer + */ + remoteAddr: Multiaddr + + /** + * Holds the lifecycle times of the connection + */ + timeline: MultiaddrConnectionTimeline + + /** + * Optional metrics counter group for this connection + */ + metrics?: CounterGroup + + /** + * 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 (init: WebRTCMultiaddrConnectionInit) { + this.remoteAddr = init.remoteAddr + this.timeline = init.timeline + this.peerConnection = init.peerConnection + + this.peerConnection.onconnectionstatechange = () => { + if (this.peerConnection.connectionState === 'closed' || this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed') { + this.timeline.close = Date.now() + } + } + } + + async close (err?: Error | undefined): Promise { + if (err !== undefined) { + log.error('error closing connection', err) + } + log.trace('closing connection') + + this.timeline.close = Date.now() + this.peerConnection.close() + this.metrics?.increment({ close: true }) + } +} diff --git a/packages/transport-webrtc/src/muxer.ts b/packages/transport-webrtc/src/muxer.ts new file mode 100644 index 0000000000..95f925cf81 --- /dev/null +++ b/packages/transport-webrtc/src/muxer.ts @@ -0,0 +1,165 @@ +import { createStream } from './stream.js' +import { nopSink, nopSource } from './util.js' +import type { DataChannelOpts } from './stream.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' +import type { Source, Sink } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +const PROTOCOL = '/webrtc' + +export interface DataChannelMuxerFactoryInit { + /** + * WebRTC Peer Connection + */ + peerConnection: RTCPeerConnection + + /** + * Optional metrics for this data channel muxer + */ + metrics?: CounterGroup + + /** + * Data channel options + */ + dataChannelOptions?: Partial + + /** + * The protocol to use + */ + protocol?: string +} + +export class DataChannelMuxerFactory implements StreamMuxerFactory { + public readonly protocol: string + + /** + * WebRTC Peer Connection + */ + private readonly peerConnection: RTCPeerConnection + private streamBuffer: Stream[] = [] + private readonly metrics?: CounterGroup + private readonly dataChannelOptions?: Partial + + constructor (init: DataChannelMuxerFactoryInit) { + this.peerConnection = init.peerConnection + this.metrics = init.metrics + this.protocol = init.protocol ?? PROTOCOL + 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.streamBuffer.push(stream) + } + } + + createStreamMuxer (init?: StreamMuxerInit): StreamMuxer { + return new DataChannelMuxer({ + ...init, + peerConnection: this.peerConnection, + dataChannelOptions: this.dataChannelOptions, + metrics: this.metrics, + streams: this.streamBuffer, + protocol: this.protocol + }) + } +} + +export interface DataChannelMuxerInit extends DataChannelMuxerFactoryInit, StreamMuxerInit { + streams: Stream[] +} + +/** + * A libp2p data channel stream muxer + */ +export class DataChannelMuxer implements StreamMuxer { + /** + * Array of streams in the data channel + */ + public streams: Stream[] + public protocol: string + + private readonly peerConnection: RTCPeerConnection + private readonly dataChannelOptions?: DataChannelOpts + private readonly metrics?: CounterGroup + + /** + * Close or abort all tracked streams and stop the muxer + */ + close: (err?: Error | undefined) => 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 + + /** + * Fired when a data channel has been added to the connection has been + * added by the remote peer. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event} + */ + this.peerConnection.ondatachannel = ({ channel }) => { + const stream = createStream({ + channel, + direction: 'inbound', + dataChannelOptions: this.dataChannelOptions, + onEnd: () => { + this.streams = this.streams.filter(s => s.id !== stream.id) + this.metrics?.increment({ stream_end: true }) + init?.onStreamEnd?.(stream) + } + }) + + this.streams.push(stream) + if ((init?.onIncomingStream) != null) { + this.metrics?.increment({ incoming_stream: true }) + init.onIncomingStream(stream) + } + } + + const onIncomingStream = init?.onIncomingStream + if (onIncomingStream != null) { + this.streams.forEach(s => { onIncomingStream(s) }) + } + } + + 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: () => { + this.streams = this.streams.filter(s => s.id !== stream.id) + this.metrics?.increment({ stream_end: true }) + this.init?.onStreamEnd?.(stream) + } + }) + this.streams.push(stream) + this.metrics?.increment({ outgoing_stream: true }) + + return stream + } +} diff --git a/packages/transport-webrtc/src/pb/message.proto b/packages/transport-webrtc/src/pb/message.proto new file mode 100644 index 0000000000..9301bd802b --- /dev/null +++ b/packages/transport-webrtc/src/pb/message.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +message Message { + enum Flag { + // The sender will no longer send messages on the stream. + FIN = 0; + + // The sender will no longer read messages on the stream. Incoming data is + // being discarded on receipt. + STOP_SENDING = 1; + + // 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; + } + + optional Flag flag = 1; + + optional bytes message = 2; +} diff --git a/packages/transport-webrtc/src/pb/message.ts b/packages/transport-webrtc/src/pb/message.ts new file mode 100644 index 0000000000..a74ca6dd06 --- /dev/null +++ b/packages/transport-webrtc/src/pb/message.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Message { + flag?: Message.Flag + message?: Uint8Array +} + +export namespace Message { + export enum Flag { + FIN = 'FIN', + STOP_SENDING = 'STOP_SENDING', + RESET = 'RESET' + } + + enum __FlagValues { + FIN = 0, + STOP_SENDING = 1, + RESET = 2 + } + + export namespace Flag { + export const codec = (): Codec => { + return enumeration(__FlagValues) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.flag != null) { + w.uint32(8) + Message.Flag.codec().encode(obj.flag, w) + } + + if (obj.message != null) { + w.uint32(18) + w.bytes(obj.message) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.flag = Message.Flag.codec().decode(reader) + break + case 2: + obj.message = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { + return decodeMessage(buf, Message.codec()) + } +} diff --git a/packages/transport-webrtc/src/private-to-private/handler.ts b/packages/transport-webrtc/src/private-to-private/handler.ts new file mode 100644 index 0000000000..176031b4fe --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/handler.ts @@ -0,0 +1,156 @@ +import { logger } from '@libp2p/logger' +import { abortableDuplex } from 'abortable-iterator' +import { pbStream } from 'it-pb-stream' +import pDefer, { type DeferredPromise } from 'p-defer' +import { DataChannelMuxerFactory } from '../muxer.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 { IncomingStreamData } from '@libp2p/interface-registrar' +import type { StreamMuxerFactory } from '@libp2p/interface-stream-muxer' + +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) + const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) + const connectedPromise: DeferredPromise = pDefer() + const answerSentPromise: DeferredPromise = pDefer() + + signal.onabort = () => { connectedPromise.reject() } + // candidate callbacks + pc.onicecandidate = ({ candidate }) => { + answerSentPromise.promise.then( + () => { + 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) + } + ) + } + + 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 + 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 } +} + +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) + 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 }) => { + stream.write({ + type: Message.Type.ICE_CANDIDATE, + data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' + }) + } + // create an offer + const offerSdp = await pc.createOffer() + // write the offer to the stream + 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 } +} + +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/listener.ts b/packages/transport-webrtc/src/private-to-private/listener.ts new file mode 100644 index 0000000000..018e569264 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/listener.ts @@ -0,0 +1,43 @@ +import { EventEmitter } from '@libp2p/interfaces/events' +import { Circuit } from '@multiformats/mafmt' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { ListenerEvents, Listener, TransportManager } from '@libp2p/interface-transport' +import type { Multiaddr } from '@multiformats/multiaddr' + +export interface ListenerOptions { + peerId: PeerId + transportManager: TransportManager +} + +export class WebRTCPeerListener extends EventEmitter implements Listener { + private readonly peerId: PeerId + private readonly transportManager: TransportManager + + constructor (opts: ListenerOptions) { + super() + + this.peerId = opts.peerId + this.transportManager = opts.transportManager + } + + async listen (): Promise { + this.safeDispatchEvent('listening', {}) + } + + getAddrs (): Multiaddr[] { + return this.transportManager + .getListeners() + .filter(l => l !== this) + .map(l => l.getAddrs() + .filter(ma => Circuit.matches(ma)) + .map(ma => { + return ma.encapsulate(`/webrtc/p2p/${this.peerId}`) + }) + ) + .flat() + } + + async close (): Promise { + this.safeDispatchEvent('close', {}) + } +} diff --git a/packages/transport-webrtc/src/private-to-private/pb/message.proto b/packages/transport-webrtc/src/private-to-private/pb/message.proto new file mode 100644 index 0000000000..ac2fa68ca6 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/pb/message.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +message Message { + // Specifies type in `data` field. + enum Type { + // String of `RTCSessionDescription.sdp` + SDP_OFFER = 0; + // String of `RTCSessionDescription.sdp` + SDP_ANSWER = 1; + // String of `RTCIceCandidate.toJSON()` + ICE_CANDIDATE = 2; + } + + optional Type type = 1; + optional string data = 2; +} \ No newline at end of file diff --git a/packages/transport-webrtc/src/private-to-private/pb/message.ts b/packages/transport-webrtc/src/private-to-private/pb/message.ts new file mode 100644 index 0000000000..b0824ed042 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/pb/message.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Message { + type?: Message.Type + data?: string +} + +export namespace Message { + export enum Type { + SDP_OFFER = 'SDP_OFFER', + SDP_ANSWER = 'SDP_ANSWER', + ICE_CANDIDATE = 'ICE_CANDIDATE' + } + + enum __TypeValues { + SDP_OFFER = 0, + SDP_ANSWER = 1, + ICE_CANDIDATE = 2 + } + + export namespace Type { + export const codec = (): Codec => { + return enumeration(__TypeValues) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.type != null) { + w.uint32(8) + Message.Type.codec().encode(obj.type, w) + } + + if (obj.data != null) { + w.uint32(18) + w.string(obj.data) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = Message.Type.codec().decode(reader) + break + case 2: + obj.data = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { + return decodeMessage(buf, Message.codec()) + } +} diff --git a/packages/transport-webrtc/src/private-to-private/transport.ts b/packages/transport-webrtc/src/private-to-private/transport.ts new file mode 100644 index 0000000000..196854d24e --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/transport.ts @@ -0,0 +1,184 @@ +import { type CreateListenerOptions, type DialOptions, type Listener, symbol, type Transport, type Upgrader, type TransportManager } from '@libp2p/interface-transport' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr' +import { codes } from '../error.js' +import { WebRTCMultiaddrConnection } from '../maconn.js' +import { initiateConnection, handleIncomingStream } from './handler.js' +import { WebRTCPeerListener } from './listener.js' +import type { DataChannelOpts } from '../stream.js' +import type { Connection } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { IncomingStreamData, Registrar } from '@libp2p/interface-registrar' +import type { Startable } from '@libp2p/interfaces/startable' + +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 interface WebRTCTransportInit { + rtcConfiguration?: RTCConfiguration + dataChannel?: Partial +} + +export interface WebRTCTransportComponents { + peerId: PeerId + registrar: Registrar + upgrader: Upgrader + transportManager: TransportManager +} + +export class WebRTCTransport implements Transport, Startable { + private _started = false + + constructor ( + private readonly components: WebRTCTransportComponents, + private readonly init: WebRTCTransportInit = {} + ) { + } + + isStarted (): boolean { + return this._started + } + + async start (): Promise { + await this.components.registrar.handle(SIGNALING_PROTO_ID, (data: IncomingStreamData) => { + this._onProtocol(data).catch(err => { log.error('failed to handle incoming connect from %p', data.connection.remotePeer, err) }) + }) + this._started = true + } + + async stop (): Promise { + await this.components.registrar.unhandle(SIGNALING_PROTO_ID) + this._started = false + } + + createListener (options: CreateListenerOptions): Listener { + return new WebRTCPeerListener(this.components) + } + + readonly [Symbol.toStringTag] = '@libp2p/webrtc' + + readonly [symbol] = true + + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + const codes = ma.protoCodes() + return codes.includes(WEBRTC_CODE) + }) + } + + /* + * dial connects to a remote via the circuit relay or any other protocol + * and proceeds to upgrade to a webrtc connection. + * multiaddr of the form: /webrtc/p2p/ + * For a circuit relay, this will be of the form + * /p2p//p2p-circuit/webrtc/p2p/ + */ + async dial (ma: Multiaddr, options: DialOptions): Promise { + log.trace('dialing address: ', ma) + const { baseAddr, peerId } = splitAddr(ma) + + if (options.signal == null) { + const controller = new AbortController() + options.signal = controller.signal + } + + const connection = await this.components.transportManager.dial(baseAddr, options) + const signalingStream = await connection.newStream([SIGNALING_PROTO_ID], options) + + try { + const { pc, muxerFactory, remoteAddress } = await initiateConnection({ + stream: signalingStream, + rtcConfiguration: this.init.rtcConfiguration, + dataChannelOptions: this.init.dataChannel, + signal: options.signal + }) + + 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 + signalingStream.close() + return result + } catch (err) { + // reset the stream in case of any error + signalingStream.reset() + throw err + } finally { + // Close the signaling connection + await connection.close() + } + } + + async _onProtocol ({ connection, stream }: IncomingStreamData): Promise { + try { + const { pc, muxerFactory, remoteAddress } = await handleIncomingStream({ + rtcConfiguration: this.init.rtcConfiguration, + connection, + stream, + dataChannelOptions: this.init.dataChannel + }) + + await this.components.upgrader.upgradeInbound(new WebRTCMultiaddrConnection({ + peerConnection: pc, + timeline: { open: (new Date()).getTime() }, + remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${connection.remotePeer.toString()}`) + }), { + skipEncryption: true, + skipProtection: true, + muxerFactory + }) + } catch (err) { + stream.reset() + throw err + } finally { + // Close the signaling connection + await connection.close() + } + } +} + +export function splitAddr (ma: Multiaddr): { baseAddr: Multiaddr, peerId: PeerId } { + const addrs = ma.toString().split(WEBRTC_TRANSPORT + '/') + if (addrs.length !== 2) { + throw new CodeError('webrtc protocol was not present in multiaddr', codes.ERR_INVALID_MULTIADDR) + } + + if (!addrs[0].includes(CIRCUIT_RELAY_TRANSPORT)) { + throw new CodeError('p2p-circuit protocol was not present in multiaddr', codes.ERR_INVALID_MULTIADDR) + } + + // look for remote peerId + let remoteAddr = multiaddr(addrs[0]) + const destination = multiaddr('/' + addrs[1]) + + const destinationIdString = destination.getPeerId() + if (destinationIdString == null) { + throw new CodeError('destination peer id was missing', codes.ERR_INVALID_MULTIADDR) + } + + const lastProtoInRemote = remoteAddr.protos().pop() + if (lastProtoInRemote === undefined) { + throw new CodeError('invalid multiaddr', codes.ERR_INVALID_MULTIADDR) + } + if (lastProtoInRemote.name !== 'p2p') { + remoteAddr = remoteAddr.encapsulate(`/p2p/${destinationIdString}`) + } + + return { baseAddr: remoteAddr, peerId: peerIdFromString(destinationIdString) } +} diff --git a/packages/transport-webrtc/src/private-to-private/util.ts b/packages/transport-webrtc/src/private-to-private/util.ts new file mode 100644 index 0000000000..e1b669778c --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/util.ts @@ -0,0 +1,59 @@ +import { logger } from '@libp2p/logger' +import { isFirefox } from '../util.js' +import { Message } from './pb/message.js' +import type { DeferredPromise } from 'p-defer' + +interface MessageStream { + read: () => Promise + write: (d: Message) => void | Promise +} + +const log = logger('libp2p:webrtc:peer:util') + +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') + } + // end of candidates has been signalled + if (message.data == null || message.data === '') { + log.trace('end-of-candidates received') + break + } + + log.trace('received new ICE candidate: %s', message.data) + try { + await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(message.data))) + } catch (err) { + log.error('bad candidate received: ', err) + throw new Error('bad candidate received') + } + } else { + // connected promise resolved + break + } + } + await connectedPromise.promise +} + +export function resolveOnConnected (pc: RTCPeerConnection, promise: DeferredPromise): void { + pc[isFirefox ? 'oniceconnectionstatechange' : 'onconnectionstatechange'] = (_) => { + log.trace('receiver peerConnectionState state: ', pc.connectionState) + switch (isFirefox ? pc.iceConnectionState : pc.connectionState) { + case 'connected': + promise.resolve() + break + case 'failed': + case 'disconnected': + case 'closed': + promise.reject(new Error('RTCPeerConnection was closed')) + break + default: + break + } + } +} diff --git a/packages/transport-webrtc/src/private-to-public/options.ts b/packages/transport-webrtc/src/private-to-public/options.ts new file mode 100644 index 0000000000..838c627ffe --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/options.ts @@ -0,0 +1,4 @@ +import type { CreateListenerOptions, DialOptions } from '@libp2p/interface-transport' + +export interface WebRTCListenerOptions extends CreateListenerOptions {} +export interface WebRTCDialOptions extends DialOptions {} diff --git a/packages/transport-webrtc/src/private-to-public/sdp.ts b/packages/transport-webrtc/src/private-to-public/sdp.ts new file mode 100644 index 0000000000..474455021f --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/sdp.ts @@ -0,0 +1,162 @@ +import { logger } from '@libp2p/logger' +import { bases } from 'multiformats/basics' +import * as multihashes from 'multihashes' +import { inappropriateMultiaddr, invalidArgument, invalidFingerprint, unsupportedHashAlgorithm } from '../error.js' +import { CERTHASH_CODE } from './transport.js' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { HashCode, HashName } from 'multihashes' + +const log = logger('libp2p:webrtc:sdp') + +/** + * Get base2 | identity decoders + */ +// @ts-expect-error - Not easy to combine these types. +export const mbdecoder: any = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b)) + +export function getLocalFingerprint (pc: RTCPeerConnection): string | undefined { + // try to fetch fingerprint from local certificate + const localCert = pc.getConfiguration().certificates?.at(0) + if (localCert == null || localCert.getFingerprints == null) { + log.trace('fetching fingerprint from local SDP') + const localDescription = pc.localDescription + if (localDescription == null) { + return undefined + } + return getFingerprintFromSdp(localDescription.sdp) + } + + log.trace('fetching fingerprint from local certificate') + + if (localCert.getFingerprints().length === 0) { + return undefined + } + + const fingerprint = localCert.getFingerprints()[0].value + if (fingerprint == null) { + throw invalidFingerprint('', 'no fingerprint on local certificate') + } + + return fingerprint +} + +const fingerprintRegex = /^a=fingerprint:(?:\w+-[0-9]+)\s(?(:?[0-9a-fA-F]{2})+)$/m +export function getFingerprintFromSdp (sdp: string): string | undefined { + const searchResult = sdp.match(fingerprintRegex) + return searchResult?.groups?.fingerprint +} +/** + * Get base2 | identity decoders + */ +function ipv (ma: Multiaddr): string { + for (const proto of ma.protoNames()) { + if (proto.startsWith('ip')) { + return proto.toUpperCase() + } + } + + log('Warning: multiaddr does not appear to contain IP4 or IP6, defaulting to IP6', ma) + + return 'IP6' +} + +// Extract the certhash from a multiaddr +export function certhash (ma: Multiaddr): string { + const tups = ma.stringTuples() + const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0] + + if (certhash === undefined || certhash === '') { + throw inappropriateMultiaddr(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`) + } + + return certhash +} + +/** + * Convert a certhash into a multihash + */ +export function decodeCerthash (certhash: string): { code: HashCode, name: HashName, length: number, digest: Uint8Array } { + const mbdecoded = mbdecoder.decode(certhash) + return multihashes.decode(mbdecoded) +} + +/** + * Extract the fingerprint from a multiaddr + */ +export function ma2Fingerprint (ma: Multiaddr): string[] { + const mhdecoded = decodeCerthash(certhash(ma)) + const prefix = toSupportedHashFunction(mhdecoded.name) + const fingerprint = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') + const sdp = fingerprint.match(/.{1,2}/g) + + if (sdp == null) { + throw invalidFingerprint(fingerprint, ma.toString()) + } + + return [`${prefix.toUpperCase()} ${sdp.join(':').toUpperCase()}`, fingerprint] +} + +/** + * Normalize the hash name from a given multihash has name + */ +export function toSupportedHashFunction (name: multihashes.HashName): string { + switch (name) { + case 'sha1': + return 'sha-1' + case 'sha2-256': + return 'sha-256' + case 'sha2-512': + return 'sha-512' + default: + throw unsupportedHashAlgorithm(name) + } +} + +/** + * Convert a multiaddr into a SDP + */ +function ma2sdp (ma: Multiaddr, ufrag: string): string { + const { host, port } = ma.toOptions() + const ipVersion = ipv(ma) + const [CERTFP] = ma2Fingerprint(ma) + + return `v=0 +o=- 0 0 IN ${ipVersion} ${host} +s=- +c=IN ${ipVersion} ${host} +t=0 0 +a=ice-lite +m=application ${port} UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=setup:passive +a=ice-ufrag:${ufrag} +a=ice-pwd:${ufrag} +a=fingerprint:${CERTFP} +a=sctp-port:5000 +a=max-message-size:100000 +a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n` +} + +/** + * Create an answer SDP from a multiaddr + */ +export function fromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit { + return { + type: 'answer', + sdp: ma2sdp(ma, ufrag) + } +} + +/** + * Replace (munge) the ufrag and password values in a SDP + */ +export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit { + if (desc.sdp === undefined) { + throw invalidArgument("Can't munge a missing SDP") + } + + desc.sdp = desc.sdp + .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n') + .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n') + return desc +} diff --git a/packages/transport-webrtc/src/private-to-public/transport.ts b/packages/transport-webrtc/src/private-to-public/transport.ts new file mode 100644 index 0000000000..c23b715c88 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/transport.ts @@ -0,0 +1,280 @@ +import { noise as Noise } from '@chainsafe/libp2p-noise' +import { type CreateListenerOptions, type Listener, symbol, type Transport } from '@libp2p/interface-transport' +import { logger } from '@libp2p/logger' +import * as p from '@libp2p/peer-id' +import { protocols } from '@multiformats/multiaddr' +import * as multihashes from 'multihashes' +import { concat } from 'uint8arrays/concat' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { dataChannelError, inappropriateMultiaddr, unimplemented, invalidArgument } from '../error.js' +import { WebRTCMultiaddrConnection } from '../maconn.js' +import { DataChannelMuxerFactory } from '../muxer.js' +import { createStream } from '../stream.js' +import { isFirefox } from '../util.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 { Connection } from '@libp2p/interface-connection' +import type { CounterGroup, Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:webrtc:transport') + +/** + * The time to wait, in milliseconds, for the data channel handshake to complete + */ +const HANDSHAKE_TIMEOUT_MS = 10_000 + +/** + * Created by converting the hexadecimal protocol code to an integer. + * + * {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv} + */ +export const WEBRTC_CODE: number = protocols('webrtc-direct').code + +/** + * Created by converting the hexadecimal protocol code to an integer. + * + * {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv} + */ +export const CERTHASH_CODE: number = protocols('certhash').code + +/** + * The peer for this transport + */ +export interface WebRTCDirectTransportComponents { + peerId: PeerId + metrics?: Metrics +} + +export interface WebRTCMetrics { + dialerEvents: CounterGroup +} + +export interface WebRTCTransportDirectInit { + dataChannel?: Partial +} + +export class WebRTCDirectTransport implements Transport { + private readonly metrics?: WebRTCMetrics + private readonly components: WebRTCDirectTransportComponents + private readonly init: WebRTCTransportDirectInit + constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) { + this.components = components + this.init = init + if (components.metrics != null) { + this.metrics = { + dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc_dialer_events_total', { + label: 'event', + help: 'Total count of WebRTC dial events by type' + }) + } + } + } + + /** + * Dial a given multiaddr + */ + async dial (ma: Multiaddr, options: WebRTCDialOptions): Promise { + const rawConn = await this._connect(ma, options) + log(`dialing address - ${ma.toString()}`) + return rawConn + } + + /** + * Create transport listeners no supported by browsers + */ + createListener (options: CreateListenerOptions): Listener { + throw unimplemented('WebRTCTransport.createListener') + } + + /** + * Takes a list of `Multiaddr`s and returns only valid addresses for the transport + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter(validMa) + } + + /** + * Implement toString() for WebRTCTransport + */ + readonly [Symbol.toStringTag] = '@libp2p/webrtc-direct' + + /** + * Symbol.for('@libp2p/transport') + */ + readonly [symbol] = true + + /** + * Connect to a peer using a multiaddr + */ + async _connect (ma: Multiaddr, options: WebRTCDialOptions): Promise { + const controller = new AbortController() + const signal = controller.signal + + const remotePeerString = ma.getPeerId() + if (remotePeerString === null) { + throw inappropriateMultiaddr("we need to have the remote's PeerId") + } + const theirPeerId = p.peerIdFromString(remotePeerString) + + const remoteCerthash = sdp.decodeCerthash(sdp.certhash(ma)) + + // ECDSA is preferred over RSA here. From our testing we find that P-256 elliptic + // curve is supported by Pion, webrtc-rs, as well as Chromium (P-228 and P-384 + // was not supported in Chromium). We use the same hash function as found in the + // multiaddr if it is supported. + const certificate = await RTCPeerConnection.generateCertificate({ + name: 'ECDSA', + namedCurve: 'P-256', + hash: sdp.toSupportedHashFunction(remoteCerthash.name) + } as any) + + const peerConnection = new RTCPeerConnection({ certificates: [certificate] }) + + // create data channel for running the noise handshake. Once the data channel is opened, + // the remote will initiate the noise handshake. This is used to confirm the identity of + // the peer. + const dataChannelOpenPromise = new Promise((resolve, reject) => { + const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 }) + const handshakeTimeout = setTimeout(() => { + const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}` + log.error(error) + this.metrics?.dialerEvents.increment({ open_error: true }) + reject(dataChannelError('data', error)) + }, HANDSHAKE_TIMEOUT_MS) + + handshakeDataChannel.onopen = (_) => { + clearTimeout(handshakeTimeout) + resolve(handshakeDataChannel) + } + + // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event + handshakeDataChannel.onerror = (event: Event) => { + clearTimeout(handshakeTimeout) + const errorTarget = event.target?.toString() ?? 'not specified' + const error = `Error opening a data channel for handshaking: ${errorTarget}` + log.error(error) + // NOTE: We use unknown error here but this could potentially be considered a reset by some standards. + this.metrics?.dialerEvents.increment({ unknown_error: true }) + reject(dataChannelError('data', error)) + } + }) + + const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32) + + // Create offer and munge sdp with ufrag == pwd. This allows the remote to + // respond to STUN messages without performing an actual SDP exchange. + // This is because it can infer the passwd field by reading the USERNAME + // attribute of the STUN message. + const offerSdp = await peerConnection.createOffer() + const mungedOfferSdp = sdp.munge(offerSdp, ufrag) + await peerConnection.setLocalDescription(mungedOfferSdp) + + // construct answer sdp from multiaddr and ufrag + const answerSdp = sdp.fromMultiAddr(ma, ufrag) + await peerConnection.setRemoteDescription(answerSdp) + + // wait for peerconnection.onopen to fire, or for the datachannel to open + const handshakeDataChannel = await dataChannelOpenPromise + + const myPeerId = this.components.peerId + + // Do noise handshake. + // Set the Noise Prologue to libp2p-webrtc-noise: before starting the actual Noise handshake. + // is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order. + const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma) + + // Since we use the default crypto interface and do not use a static key or early data, + // we pass in undefined for these parameters. + const noise = Noise({ prologueBytes: fingerprintsPrologue })() + + const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel }) + const wrappedDuplex = { + ...wrappedChannel, + sink: wrappedChannel.sink.bind(wrappedChannel), + source: (async function * () { + for await (const list of wrappedChannel.source) { + for (const buf of list) { + yield buf + } + } + }()) + } + + // Creating the connection before completion of the noise + // handshake ensures that the stream opening callback is set up + const maConn = new WebRTCMultiaddrConnection({ + peerConnection, + remoteAddr: ma, + timeline: { + open: Date.now() + }, + metrics: this.metrics?.dialerEvents + }) + + const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange' + + peerConnection.addEventListener(eventListeningName, () => { + switch (peerConnection.connectionState) { + case 'failed': + case 'disconnected': + case 'closed': + maConn.close().catch((err) => { + log.error('error closing connection', err) + }).finally(() => { + // Remove the event listener once the connection is closed + controller.abort() + }) + break + default: + break + } + }, { signal }) + + // Track opened peer connection + this.metrics?.dialerEvents.increment({ peer_connection: true }) + + const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel }) + + // For outbound connections, the remote is expected to start the noise handshake. + // Therefore, we need to secure an inbound noise connection from the remote. + await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId) + + return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory }) + } + + /** + * Generate a noise prologue from the peer connection's certificate. + * noise prologue = bytes('libp2p-webrtc-noise:') + noise-responder fingerprint + noise-initiator fingerprint + */ + private generateNoisePrologue (pc: RTCPeerConnection, hashCode: multihashes.HashCode, ma: Multiaddr): Uint8Array { + if (pc.getConfiguration().certificates?.length === 0) { + throw invalidArgument('no local certificate') + } + + const localFingerprint = sdp.getLocalFingerprint(pc) + if (localFingerprint == null) { + throw invalidArgument('no local fingerprint found') + } + + const localFpString = localFingerprint.trim().toLowerCase().replaceAll(':', '') + const localFpArray = uint8arrayFromString(localFpString, 'hex') + const local = multihashes.encode(localFpArray, hashCode) + const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(ma)) + const prefix = uint8arrayFromString('libp2p-webrtc-noise:') + + 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/private-to-public/util.ts b/packages/transport-webrtc/src/private-to-public/util.ts new file mode 100644 index 0000000000..6ef40af9df --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/util.ts @@ -0,0 +1,3 @@ + +const charset = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') +export const genUfrag = (len: number): string => [...Array(len)].map(() => charset.at(Math.floor(Math.random() * charset.length))).join('') diff --git a/packages/transport-webrtc/src/stream.ts b/packages/transport-webrtc/src/stream.ts new file mode 100644 index 0000000000..fe5482bca3 --- /dev/null +++ b/packages/transport-webrtc/src/stream.ts @@ -0,0 +1,273 @@ +import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import * as lengthPrefixed from 'it-length-prefixed' +import { type Pushable, pushable } from 'it-pushable' +import { pEvent, TimeoutError } from 'p-event' +import { Uint8ArrayList } from 'uint8arraylist' +import { Message } from './pb/message.js' +import type { Direction, Stream } from '@libp2p/interface-connection' + +const log = logger('libp2p:webrtc:stream') + +export interface DataChannelOpts { + maxMessageSize: number + maxBufferedAmount: number + bufferedAmountLowEventTimeout: number +} + +export interface WebRTCStreamInit extends AbstractStreamInit { + /** + * The network channel used for bidirectional peer-to-peer transfers of + * arbitrary data + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} + */ + channel: RTCDataChannel + + dataChannelOptions?: Partial +} + +// 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 + +class WebRTCStream extends AbstractStream { + /** + * The data channel used to send and receive data + */ + 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. + */ + private readonly incomingData: Pushable + + private messageQueue?: Uint8ArrayList + + constructor (init: WebRTCStreamInit) { + 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 + } + + // set up initial state + switch (this.channel.readyState) { + case 'open': + break + + case 'closed': + case 'closing': + if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) { + this.stat.timeline.close = Date.now() + } + break + case 'connecting': + // noop + break + + default: + log.error('unknown datachannel state %s', this.channel.readyState) + throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') + } + + // handle RTCDataChannel events + this.channel.onopen = (_evt) => { + this.stat.timeline.open = new Date().getTime() + + if (this.messageQueue != null) { + // send any queued messages + this._sendMessage(this.messageQueue) + .catch(err => { + this.abort(err) + }) + this.messageQueue = undefined + } + } + + this.channel.onclose = (_evt) => { + this.close() + } + + this.channel.onerror = (evt) => { + const err = (evt as RTCErrorEvent).error + this.abort(err) + } + + const self = this + + this.channel.onmessage = async (event: MessageEvent) => { + const { data } = event + + if (data === null || data.byteLength === 0) { + return + } + + this.incomingData.push(new Uint8Array(data, 0, data.byteLength)) + } + + // 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()) + + if (message != null) { + self.sourcePush(new Uint8ArrayList(message)) + } + } + }) + .catch(err => { + log.error('error processing incoming data channel messages', err) + }) + } + + sendNewStream (): void { + // opening new streams is handled by WebRTC so this is a noop + } + + async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise { + if (checkBuffer && this.channel.bufferedAmount > this.dataChannelOptions.maxBufferedAmount) { + try { + await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout }) + } catch (err: any) { + if (err instanceof TimeoutError) { + this.abort(err) + throw new Error('Timed out waiting for DataChannel buffer to clear') + } + + throw err + } + } + + if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') { + throw new CodeError('Invalid datachannel state - closed or closing', 'ERR_INVALID_STATE') + } + + if (this.channel.readyState === 'open') { + // send message without copying data + for (const buf of data) { + this.channel.send(buf) + } + } else if (this.channel.readyState === 'connecting') { + // queue message for when we are open + if (this.messageQueue == null) { + this.messageQueue = new Uint8ArrayList() + } + + this.messageQueue.append(data) + } else { + log.error('unknown datachannel state %s', this.channel.readyState) + throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') + } + } + + async sendData (data: Uint8ArrayList): Promise { + const msgbuf = Message.encode({ message: data.subarray() }) + const sendbuf = lengthPrefixed.encode.single(msgbuf) + + await this._sendMessage(sendbuf) + } + + async sendReset (): Promise { + await this._sendFlag(Message.Flag.RESET) + } + + async sendCloseWrite (): Promise { + await this._sendFlag(Message.Flag.FIN) + } + + async sendCloseRead (): Promise { + await this._sendFlag(Message.Flag.STOP_SENDING) + } + + /** + * Handle incoming + */ + private processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined { + const message = Message.decode(buffer) + + if (message.flag !== undefined) { + if (message.flag === Message.Flag.FIN) { + // We should expect no more data from the remote, stop reading + this.incomingData.end() + this.closeRead() + } + + if (message.flag === Message.Flag.RESET) { + // Stop reading and writing to the stream immediately + this.reset() + } + + if (message.flag === Message.Flag.STOP_SENDING) { + // The remote has stopped reading + this.closeWrite() + } + } + + return message.message + } + + private async _sendFlag (flag: Message.Flag): Promise { + log.trace('Sending flag: %s', flag.toString()) + const msgbuf = Message.encode({ flag }) + const prefixedBuf = lengthPrefixed.encode.single(msgbuf) + + await this._sendMessage(prefixedBuf, false) + } +} + +export interface WebRTCStreamOptions { + /** + * The network channel used for bidirectional peer-to-peer transfers of + * arbitrary data + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} + */ + channel: RTCDataChannel + + /** + * The stream direction + */ + direction: Direction + + dataChannelOptions?: Partial + + maxMsgSize?: number + + onEnd?: (err?: Error | undefined) => void +} + +export function createStream (options: WebRTCStreamOptions): Stream { + const { channel, direction, onEnd, dataChannelOptions } = 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 + }) +} diff --git a/packages/transport-webrtc/src/util.ts b/packages/transport-webrtc/src/util.ts new file mode 100644 index 0000000000..e26e64dd5f --- /dev/null +++ b/packages/transport-webrtc/src/util.ts @@ -0,0 +1,8 @@ +import { detect } from 'detect-browser' + +const browser = detect() +export const isFirefox = ((browser != null) && browser.name === 'firefox') + +export const nopSource = async function * nop (): AsyncGenerator {} + +export const nopSink = async (_: any): Promise => {} diff --git a/packages/transport-webrtc/test/basics.spec.ts b/packages/transport-webrtc/test/basics.spec.ts new file mode 100644 index 0000000000..ec15c43703 --- /dev/null +++ b/packages/transport-webrtc/test/basics.spec.ts @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { webSockets } from '@libp2p/websockets' +import * as filter from '@libp2p/websockets/filters' +import { WebRTC } from '@multiformats/mafmt' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import map from 'it-map' +import { pipe } from 'it-pipe' +import toBuffer from 'it-to-buffer' +import { createLibp2p } from 'libp2p' +import { circuitRelayTransport } from 'libp2p/circuit-relay' +import { identifyService } from 'libp2p/identify' +import { webRTC } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Libp2p } from '@libp2p/interface-libp2p' + +async function createNode (): Promise { + return createLibp2p({ + addresses: { + listen: [ + '/webrtc', + `${process.env.RELAY_MULTIADDR}/p2p-circuit` + ] + }, + transports: [ + webSockets({ + filter: filter.all + }), + circuitRelayTransport(), + webRTC() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + services: { + identify: identifyService() + }, + connectionGater: { + denyDialMultiaddr: () => false + }, + connectionManager: { + minConnections: 0 + } + }) +} + +describe('basics', () => { + const echo = '/echo/1.0.0' + + let localNode: Libp2p + let remoteNode: Libp2p + + async function connectNodes (): Promise { + const remoteAddr = remoteNode.getMultiaddrs() + .filter(ma => WebRTC.matches(ma)).pop() + + if (remoteAddr == null) { + throw new Error('Remote peer could not listen on relay') + } + + await remoteNode.handle(echo, ({ stream }) => { + void pipe( + stream, + stream + ) + }) + + const connection = await localNode.dial(remoteAddr) + + // disconnect both from relay + await localNode.hangUp(multiaddr(process.env.RELAY_MULTIADDR)) + await remoteNode.hangUp(multiaddr(process.env.RELAY_MULTIADDR)) + + return connection + } + + beforeEach(async () => { + localNode = await createNode() + remoteNode = await createNode() + }) + + afterEach(async () => { + if (localNode != null) { + await localNode.stop() + } + + if (remoteNode != null) { + await remoteNode.stop() + } + }) + + it('can dial through a relay', async () => { + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo) + + // send and receive some data + const input = new Array(5).fill(0).map(() => new Uint8Array(10)) + const output = await pipe( + input, + stream, + (source) => map(source, list => list.subarray()), + async (source) => toBuffer(source) + ) + + // asset that we got the right data + expect(output).to.equalBytes(toBuffer(input)) + }) + + it('can send a large file', async () => { + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo) + + // send and receive some data + const input = new Array(5).fill(0).map(() => new Uint8Array(1024 * 1024)) + const output = await pipe( + input, + stream, + (source) => map(source, list => list.subarray()), + async (source) => toBuffer(source) + ) + + // asset that we got the right data + expect(output).to.equalBytes(toBuffer(input)) + }) +}) diff --git a/packages/transport-webrtc/test/listener.spec.ts b/packages/transport-webrtc/test/listener.spec.ts new file mode 100644 index 0000000000..4928ca682c --- /dev/null +++ b/packages/transport-webrtc/test/listener.spec.ts @@ -0,0 +1,40 @@ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { stubInterface } from 'sinon-ts' +import { WebRTCPeerListener } from '../src/private-to-private/listener' +import type { Listener, TransportManager } from '@libp2p/interface-transport' + +describe('webrtc private-to-private listener', () => { + it('should only return relay addresses as webrtc listen addresses', async () => { + const relayedAddress = '/ip4/127.0.0.1/tcp/4034/ws/p2p-circuit' + const otherListenAddress = '/ip4/127.0.0.1/tcp/4001' + const peerId = await createEd25519PeerId() + const transportManager = stubInterface() + + const listener = new WebRTCPeerListener({ + peerId, + transportManager + }) + + const otherListener = stubInterface({ + getAddrs: [multiaddr(otherListenAddress)] + }) + + const relayListener = stubInterface({ + getAddrs: [multiaddr(relayedAddress)] + }) + + transportManager.getListeners.returns([ + listener, + otherListener, + relayListener + ]) + + const addresses = listener.getAddrs() + + expect(addresses.map(ma => ma.toString())).to.deep.equal([ + `${relayedAddress}/webrtc/p2p/${peerId}` + ]) + }) +}) diff --git a/packages/transport-webrtc/test/maconn.browser.spec.ts b/packages/transport-webrtc/test/maconn.browser.spec.ts new file mode 100644 index 0000000000..9a25dc541c --- /dev/null +++ b/packages/transport-webrtc/test/maconn.browser.spec.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { stubObject } from 'sinon-ts' +import { WebRTCMultiaddrConnection } from '../src/maconn.js' +import type { CounterGroup } from '@libp2p/interface-metrics' + +describe('Multiaddr Connection', () => { + it('can open and close', async () => { + const peerConnection = new RTCPeerConnection() + peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) + const remoteAddr = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') + const metrics = stubObject({ + increment: () => {}, + reset: () => {} + }) + const maConn = new WebRTCMultiaddrConnection({ + peerConnection, + remoteAddr, + timeline: { + open: (new Date()).getTime() + }, + metrics + }) + + expect(maConn.timeline.close).to.be.undefined + + await maConn.close() + + expect(maConn.timeline.close).to.not.be.undefined + expect(metrics.increment.calledWith({ close: true })).to.be.true + }) +}) diff --git a/packages/transport-webrtc/test/peer.browser.spec.ts b/packages/transport-webrtc/test/peer.browser.spec.ts new file mode 100644 index 0000000000..25fe8284bb --- /dev/null +++ b/packages/transport-webrtc/test/peer.browser.spec.ts @@ -0,0 +1,132 @@ +import { mockConnection, mockMultiaddrConnection, mockRegistrar, mockStream, mockUpgrader } from '@libp2p/interface-mocks' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { 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-pb-stream' +import Sinon from 'sinon' +import { initiateConnection, handleIncomingStream } from '../src/private-to-private/handler' +import { Message } from '../src/private-to-private/pb/message.js' +import { WebRTCTransport, splitAddr } from '../src/private-to-private/transport' + +const browser = detect() + +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]) + if (isFirefox) { + expect(pc0.iceConnectionState).eq('connected') + expect(pc1.iceConnectionState).eq('connected') + return + } + expect(pc0.connectionState).eq('connected') + expect(pc1.connectionState).eq('connected') + }) +}) + +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) + + stream.write({ type: Message.Type.SDP_OFFER, data: 'bad' }) + await expect(receiverPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) + }) +}) + +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 offerMessage = await stream.read() + expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) + } + + stream.write({ type: Message.Type.SDP_ANSWER, data: 'bad' }) + await expect(initiatorPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) + }) + + 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 pc = new RTCPeerConnection() + pc.onicecandidate = ({ candidate }) => { + stream.write({ type: Message.Type.ICE_CANDIDATE, data: JSON.stringify(candidate?.toJSON()) }) + } + { + const offerMessage = await stream.read() + expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) + const offer = new RTCSessionDescription({ type: 'offer', sdp: offerMessage.data }) + await pc.setRemoteDescription(offer) + + const answer = await pc.createAnswer() + await pc.setLocalDescription(answer) + } + + await expect(initiatorPeerConnectionPromise).to.be.rejectedWith(/remote should send an SDP answer/) + }) +}) + +describe('webrtc filter', () => { + it('can filter multiaddrs to dial', async () => { + const transport = new WebRTCTransport({ + transportManager: Sinon.stub() as any, + peerId: Sinon.stub() as any, + registrar: mockRegistrar(), + upgrader: mockUpgrader({}) + }, {}) + + const valid = [ + multiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p-circuit/webrtc') + ] + + expect(transport.filter(valid)).length(1) + }) +}) + +describe('webrtc splitAddr', () => { + it('can split a ws relay addr', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/49173/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/webrtc/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') + + const { baseAddr, peerId } = splitAddr(ma) + + expect(baseAddr.toString()).to.eq('/ip4/127.0.0.1/tcp/49173/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') + expect(peerId.toString()).to.eq('12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') + }) + + it('can split a webrtc-direct relay addr', async () => { + const ma = multiaddr('/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiBUr89tH2P9paTCPn-AcfVZcgvIvkwns96t4h55IpxFtA/p2p/12D3KooWB64sJqc3T3VCaubQCrfCvvfummrAA9z1vEXHJT77ZNJh/p2p-circuit/webrtc/p2p/12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') + + const { baseAddr, peerId } = splitAddr(ma) + + expect(baseAddr.toString()).to.eq('/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiBUr89tH2P9paTCPn-AcfVZcgvIvkwns96t4h55IpxFtA/p2p/12D3KooWB64sJqc3T3VCaubQCrfCvvfummrAA9z1vEXHJT77ZNJh/p2p-circuit/p2p/12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') + expect(peerId.toString()).to.eq('12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') + }) +}) + +export { } diff --git a/packages/transport-webrtc/test/sdp.spec.ts b/packages/transport-webrtc/test/sdp.spec.ts new file mode 100644 index 0000000000..2c32f7967b --- /dev/null +++ b/packages/transport-webrtc/test/sdp.spec.ts @@ -0,0 +1,81 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import * as underTest from '../src/private-to-public/sdp.js' + +const sampleMultiAddr = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') +const sampleCerthash = 'uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ' +const sampleSdp = `v=0 +o=- 0 0 IN IP4 0.0.0.0 +s=- +c=IN IP4 0.0.0.0 +t=0 0 +a=ice-lite +m=application 56093 UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=setup:passive +a=ice-ufrag:MyUserFragment +a=ice-pwd:MyUserFragment +a=fingerprint:SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1 +a=sctp-port:5000 +a=max-message-size:100000 +a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` + +describe('SDP', () => { + it('converts multiaddr with certhash to an answer SDP', async () => { + const ufrag = 'MyUserFragment' + const sdp = underTest.fromMultiAddr(sampleMultiAddr, ufrag) + + expect(sdp.sdp).to.contain(sampleSdp) + }) + + it('extracts certhash from a multiaddr', () => { + const certhash = underTest.certhash(sampleMultiAddr) + + expect(certhash).to.equal(sampleCerthash) + }) + + it('decodes a certhash', () => { + const decoded = underTest.decodeCerthash(sampleCerthash) + + // sha2-256 multihash 0x12 permanent + // https://github.com/multiformats/multicodec/blob/master/table.csv + expect(decoded.name).to.equal('sha2-256') + expect(decoded.code).to.equal(0x12) + expect(decoded.length).to.equal(32) + expect(decoded.digest.toString()).to.equal('114,104,71,205,72,176,94,197,96,77,21,156,191,64,29,111,0,161,35,236,144,23,14,44,209,179,143,210,157,55,229,177') + }) + + it('converts a multiaddr into a fingerprint', () => { + const fingerpint = underTest.ma2Fingerprint(sampleMultiAddr) + expect(fingerpint).to.deep.equal([ + 'SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1', + '726847cd48b05ec5604d159cbf401d6f00a123ec90170e2cd1b38fd29d37e5b1' + ]) + }) + + it('extracts a fingerprint from sdp', () => { + const fingerprint = underTest.getFingerprintFromSdp(sampleSdp) + expect(fingerprint).to.eq('72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1') + }) + + it('munges the ufrag and pwd in a SDP', () => { + const result = underTest.munge({ type: 'answer', sdp: sampleSdp }, 'someotheruserfragmentstring') + const expected = `v=0 +o=- 0 0 IN IP4 0.0.0.0 +s=- +c=IN IP4 0.0.0.0 +t=0 0 +a=ice-lite +m=application 56093 UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=setup:passive +a=ice-ufrag:someotheruserfragmentstring +a=ice-pwd:someotheruserfragmentstring +a=fingerprint:SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1 +a=sctp-port:5000 +a=max-message-size:100000 +a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` + + expect(result.sdp).to.equal(expected) + }) +}) diff --git a/packages/transport-webrtc/test/stream.browser.spec.ts b/packages/transport-webrtc/test/stream.browser.spec.ts new file mode 100644 index 0000000000..ee57a61d84 --- /dev/null +++ b/packages/transport-webrtc/test/stream.browser.spec.ts @@ -0,0 +1,150 @@ +import { expect } from 'aegir/chai' +import delay from 'delay' +import * as lengthPrefixed from 'it-length-prefixed' +import { bytes } from 'multiformats' +import { Message } from '../src/pb/message.js' +import { createStream } from '../src/stream' +import type { Stream } from '@libp2p/interface-connection' +const TEST_MESSAGE = 'test_message' + +function setup (): { peerConnection: RTCPeerConnection, dataChannel: RTCDataChannel, stream: Stream } { + const peerConnection = new RTCPeerConnection() + const dataChannel = peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) + const stream = createStream({ channel: dataChannel, direction: 'outbound' }) + + return { peerConnection, dataChannel, stream } +} + +function generatePbByFlag (flag?: Message.Flag): Uint8Array { + const buf = Message.encode({ + flag, + message: bytes.fromString(TEST_MESSAGE) + }) + + return lengthPrefixed.encode.single(buf).subarray() +} + +describe('Stream Stats', () => { + let stream: Stream + + beforeEach(async () => { + ({ stream } = setup()) + }) + + it('can construct', () => { + expect(stream.stat.timeline.close).to.not.exist() + }) + + it('close marks it closed', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.close() + expect(stream.stat.timeline.close).to.be.a('number') + }) + + it('closeRead marks it read-closed only', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.closeRead() + expect(stream.stat.timeline.close).to.not.exist() + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('closeWrite marks it write-closed only', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.closeWrite() + expect(stream.stat.timeline.close).to.not.exist() + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('closeWrite AND closeRead = close', async () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.closeWrite() + stream.closeRead() + expect(stream.stat.timeline.close).to.be.a('number') + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('abort = close', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.abort(new Error('Oh no!')) + expect(stream.stat.timeline.close).to.be.a('number') + expect(stream.stat.timeline.close).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('reset = close', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.reset() // only resets the write side + expect(stream.stat.timeline.close).to.be.a('number') + expect(stream.stat.timeline.close).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) +}) + +describe('Stream Read Stats Transition By Incoming Flag', () => { + let dataChannel: RTCDataChannel + let stream: Stream + + beforeEach(async () => { + ({ dataChannel, stream } = setup()) + }) + + it('no flag, no transition', () => { + expect(stream.stat.timeline.close).to.not.exist() + const data = generatePbByFlag() + dataChannel.onmessage?.(new MessageEvent('message', { data })) + + expect(stream.stat.timeline.close).to.not.exist() + }) + + it('open to read-close by flag:FIN', async () => { + const data = generatePbByFlag(Message.Flag.FIN) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.not.exist() + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('read-close to close by flag:STOP_SENDING', async () => { + const data = generatePbByFlag(Message.Flag.STOP_SENDING) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.not.exist() + }) +}) + +describe('Stream Write Stats Transition By Incoming Flag', () => { + let dataChannel: RTCDataChannel + let stream: Stream + + beforeEach(async () => { + ({ dataChannel, stream } = setup()) + }) + + it('open to write-close by flag:STOP_SENDING', async () => { + const data = generatePbByFlag(Message.Flag.STOP_SENDING) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.not.exist() + }) + + it('write-close to close by flag:FIN', async () => { + const data = generatePbByFlag(Message.Flag.FIN) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.not.exist() + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) +}) diff --git a/packages/transport-webrtc/test/stream.spec.ts b/packages/transport-webrtc/test/stream.spec.ts new file mode 100644 index 0000000000..dd44b1ebab --- /dev/null +++ b/packages/transport-webrtc/test/stream.spec.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +import { expect } from 'aegir/chai' +import length from 'it-length' +import * as lengthPrefixed from 'it-length-prefixed' +import { pushable } from 'it-pushable' +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 + +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 p = pushable() + + // 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' + }) + + p.push(data) + p.end() + await webrtcStream.sink(p) + + // length(message) + message + length(FIN) + FIN + expect(length(sent)).to.equal(4) + + for (const buf of sent) { + expect(buf.byteLength).to.be.lessThanOrEqual(MAX_MESSAGE_SIZE) + } + }) + + it(`sends messages greater than ${MAX_MESSAGE_SIZE} bytes in parts`, async () => { + const sent: Uint8ArrayList = new Uint8ArrayList() + const data = new Uint8Array(MAX_MESSAGE_SIZE) + const p = pushable() + + // 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) + } + }), + direction: 'outbound' + }) + + p.push(data) + p.end() + await webrtcStream.sink(p) + + expect(length(sent)).to.equal(6) + + for (const buf of sent) { + expect(buf.byteLength).to.be.lessThanOrEqual(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 + }, + channel: mockDataChannel({ + send: () => { + throw new Error('Expected to not send') + }, + bufferedAmount: MAX_BUFFERED_AMOUNT + }), + direction: 'outbound', + onEnd: () => { + closed = true + } + }) + + const p = pushable() + p.push(new Uint8Array(1)) + p.end() + + const t0 = Date.now() + + await expect(webrtcStream.sink(p)).to.eventually.be.rejected + .with.property('message', 'Timed out waiting for DataChannel buffer to clear') + const t1 = Date.now() + expect(t1 - t0).greaterThan(timeout) + expect(t1 - t0).lessThan(timeout + 1000) // Some upper bound + expect(closed).true() + }) +}) diff --git a/packages/transport-webrtc/test/transport.browser.spec.ts b/packages/transport-webrtc/test/transport.browser.spec.ts new file mode 100644 index 0000000000..dea6c68fbd --- /dev/null +++ b/packages/transport-webrtc/test/transport.browser.spec.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ + +import { mockMetrics, mockUpgrader } from '@libp2p/interface-mocks' +import { type CreateListenerOptions, symbol } from '@libp2p/interface-transport' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' +import { expect, assert } from 'aegir/chai' +import { UnimplementedError } from '../src/error.js' +import * as underTest from '../src/private-to-public/transport.js' +import { expectError } from './util.js' +import type { Metrics } from '@libp2p/interface-metrics' + +function ignoredDialOption (): CreateListenerOptions { + const upgrader = mockUpgrader({}) + return { upgrader } +} + +describe('WebRTC Transport', () => { + let metrics: Metrics + let components: underTest.WebRTCDirectTransportComponents + + before(async () => { + metrics = mockMetrics()() + components = { + peerId: await createEd25519PeerId(), + metrics + } + }) + + it('can construct', () => { + const t = new underTest.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 options = ignoredDialOption() + + // don't await as this isn't an e2e test + transport.dial(ma, options) + }) + + it('createListner throws', () => { + const t = new underTest.WebRTCDirectTransport(components) + try { + t.createListener(ignoredDialOption()) + expect('Should have thrown').to.equal('but did not') + } catch (e) { + expect(e).to.be.instanceOf(UnimplementedError) + } + }) + + it('toString property getter', () => { + const t = new underTest.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 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 = + multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/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() + }) + + 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) + + try { + await transport.dial(ma, ignoredDialOption()) + } catch (error) { + const expected = 'WebRTC transport error: There was a problem with the Multiaddr which was passed in: we need to have the remote\'s PeerId' + expectError(error, expected) + } + }) +}) diff --git a/packages/transport-webrtc/test/util.ts b/packages/transport-webrtc/test/util.ts new file mode 100644 index 0000000000..70c492b6a7 --- /dev/null +++ b/packages/transport-webrtc/test/util.ts @@ -0,0 +1,9 @@ +import { expect } from 'aegir/chai' + +export const expectError = (error: unknown, message: string): void => { + if (error instanceof Error) { + expect(error.message).to.equal(message) + } else { + expect('Did not throw error:').to.equal(message) + } +} diff --git a/packages/transport-webrtc/tsconfig.json b/packages/transport-webrtc/tsconfig.json new file mode 100644 index 0000000000..aa7a3a98ba --- /dev/null +++ b/packages/transport-webrtc/tsconfig.json @@ -0,0 +1,55 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test", + "proto_ts" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-libp2p" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-registrar" + }, + { + "path": "../interface-stream-muxer" + }, + { + "path": "../interface-transport" + }, + { + "path": "../interfaces" + }, + { + "path": "../libp2p" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../transport-websockets" + } + ] +} diff --git a/packages/transport-websockets/.aegir.js b/packages/transport-websockets/.aegir.js new file mode 100644 index 0000000000..19b4335f8f --- /dev/null +++ b/packages/transport-websockets/.aegir.js @@ -0,0 +1,46 @@ +import { pipe } from 'it-pipe' + +/** @type {import('aegir/types').PartialOptions} */ +export default { + test: { + async before () { + const { multiaddr } = await import('@multiformats/multiaddr') + const { mockRegistrar, mockUpgrader } = await import('@libp2p/interface-mocks') + const { EventEmitter } = await import('@libp2p/interfaces/events') + const { webSockets } = await import('./dist/src/index.js') + + const protocol = '/echo/1.0.0' + const registrar = mockRegistrar() + registrar.handle(protocol, ({ stream }) => { + void pipe( + stream, + stream + ) + }) + const upgrader = mockUpgrader({ + registrar, + events: new EventEmitter() + }) + + const ws = webSockets()() + const ma = multiaddr('/ip4/127.0.0.1/tcp/9095/ws') + const listener = ws.createListener({ + upgrader + }) + await listener.listen(ma) + listener.addEventListener('error', (evt) => { + console.error(evt.detail) + }) + + return { + listener + } + }, + async after (_, before) { + await before.listener.close() + } + }, + build: { + bundlesizeMax: '18kB' + } +} diff --git a/packages/transport-websockets/CHANGELOG.md b/packages/transport-websockets/CHANGELOG.md new file mode 100644 index 0000000000..b218a6315a --- /dev/null +++ b/packages/transport-websockets/CHANGELOG.md @@ -0,0 +1,713 @@ +## [6.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v6.0.2...v6.0.3) (2023-06-06) + + +### Dependencies + +* **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)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v6.0.1...v6.0.2) (2023-06-06) + + +### Dependencies + +* add ws types to main dependencies ([#246](https://github.com/libp2p/js-libp2p-websockets/issues/246)) ([628e260](https://github.com/libp2p/js-libp2p-websockets/commit/628e26049c2ee3695d0393a9a69f3a80ced820e7)), closes [/github.com/alanshaw/it-ws/blob/master/package.json#L50](https://github.com/libp2p//github.com/alanshaw/it-ws/blob/master/package.json/issues/L50) + +## [6.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v6.0.0...v6.0.1) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-transport from 3.0.0 to 4.0.0 ([#233](https://github.com/libp2p/js-libp2p-websockets/issues/233)) ([da0367f](https://github.com/libp2p/js-libp2p-websockets/commit/da0367f7a02a23a819d9a768ab209a53e791ae6b)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.10...v6.0.0) (2023-04-18) + + +### ⚠ BREAKING CHANGES + +* the type of the source/sink properties have changed + +### Dependencies + +* update stream types ([#232](https://github.com/libp2p/js-libp2p-websockets/issues/232)) ([5a69d38](https://github.com/libp2p/js-libp2p-websockets/commit/5a69d38f68f0796f869d8becb5d02be1772cda94)) + +## [5.0.10](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.9...v5.0.10) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#231](https://github.com/libp2p/js-libp2p-websockets/issues/231)) ([e2f7204](https://github.com/libp2p/js-libp2p-websockets/commit/e2f72040a214efd35215a5919aa92fec5038ca11)) + +## [5.0.9](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.8...v5.0.9) (2023-04-10) + + +### Bug Fixes + +* correction package.json exports types path ([#230](https://github.com/libp2p/js-libp2p-websockets/issues/230)) ([0a6ed35](https://github.com/libp2p/js-libp2p-websockets/commit/0a6ed354a131ff00836143611afc18978b08013b)) + +## [5.0.8](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.7...v5.0.8) (2023-03-31) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#226](https://github.com/libp2p/js-libp2p-websockets/issues/226)) ([8e9affd](https://github.com/libp2p/js-libp2p-websockets/commit/8e9affdd6ddda9a2678a143fb9390cf6917a8aff)) + +## [5.0.7](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.6...v5.0.7) (2023-03-20) + + +### Dependencies + +* bump @multiformats/mafmt from 11.1.2 to 12.0.0 ([#224](https://github.com/libp2p/js-libp2p-websockets/issues/224)) ([ee3bb05](https://github.com/libp2p/js-libp2p-websockets/commit/ee3bb054ac265210c436defb42c62fd5de969232)) + +## [5.0.6](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.5...v5.0.6) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#223](https://github.com/libp2p/js-libp2p-websockets/issues/223)) ([e69a70c](https://github.com/libp2p/js-libp2p-websockets/commit/e69a70c1004d25a1272877ecba1ba2af1afc5a2f)) + +## [5.0.5](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.4...v5.0.5) (2023-03-09) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([4936a70](https://github.com/libp2p/js-libp2p-websockets/commit/4936a70ef1d148aab69c4c659ba2e3a7724918d6)) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 8.0.5 to 9.1.3 ([#220](https://github.com/libp2p/js-libp2p-websockets/issues/220)) ([1076b05](https://github.com/libp2p/js-libp2p-websockets/commit/1076b05ccd3bc3f5a0115759baa1f438f66416da)) +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#219](https://github.com/libp2p/js-libp2p-websockets/issues/219)) ([4e0f17b](https://github.com/libp2p/js-libp2p-websockets/commit/4e0f17b04ba4f62b72d91a3c343d748d79ca4000)) + +## [5.0.4](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.3...v5.0.4) (2023-03-03) + + +### Bug Fixes + +* Only filter by wss not dns ([#218](https://github.com/libp2p/js-libp2p-websockets/issues/218)) ([434d44c](https://github.com/libp2p/js-libp2p-websockets/commit/434d44cdfcbab48008a160cca2da1129cb43860b)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([fad99cc](https://github.com/libp2p/js-libp2p-websockets/commit/fad99cca84037922b62785829fb67c6110ea4c16)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([b1954aa](https://github.com/libp2p/js-libp2p-websockets/commit/b1954aad07080a24db9021ea2736d34a4e444d00)) + +## [5.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.2...v5.0.3) (2023-01-13) + + +### Dependencies + +* remove err-code ([#202](https://github.com/libp2p/js-libp2p-websockets/issues/202)) ([40ce006](https://github.com/libp2p/js-libp2p-websockets/commit/40ce0060918cb390b343a748036af4aee43b2146)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.1...v5.0.2) (2022-12-16) + + +### Documentation + +* publish api docs ([#201](https://github.com/libp2p/js-libp2p-websockets/issues/201)) ([722b03a](https://github.com/libp2p/js-libp2p-websockets/commit/722b03a7a57200505aebac018aeb06ba85219721)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.0...v5.0.1) (2022-12-08) + + +### Bug Fixes + +* cannot catch EADDRINUSE ([#198](https://github.com/libp2p/js-libp2p-websockets/issues/198)) ([c7312db](https://github.com/libp2p/js-libp2p-websockets/commit/c7312db639b37c767afe0651cab9d33a6f0246b3)), closes [#184](https://github.com/libp2p/js-libp2p-websockets/issues/184) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 7.1.0 to 8.0.2 ([#199](https://github.com/libp2p/js-libp2p-websockets/issues/199)) ([daff533](https://github.com/libp2p/js-libp2p-websockets/commit/daff53335baec84ae97d937ab79779f475c8ab18)), closes [#318](https://github.com/libp2p/js-libp2p-websockets/issues/318) [#315](https://github.com/libp2p/js-libp2p-websockets/issues/315) [#313](https://github.com/libp2p/js-libp2p-websockets/issues/313) [#312](https://github.com/libp2p/js-libp2p-websockets/issues/312) +* **dev:** bump it-all from 1.0.6 to 2.0.0 ([#193](https://github.com/libp2p/js-libp2p-websockets/issues/193)) ([6213f8f](https://github.com/libp2p/js-libp2p-websockets/commit/6213f8f6f113846622c53966478ae75c81fa5d14)), closes [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#27](https://github.com/libp2p/js-libp2p-websockets/issues/27) [#24](https://github.com/libp2p/js-libp2p-websockets/issues/24) +* **dev:** bump it-drain from 1.0.5 to 2.0.0 ([#191](https://github.com/libp2p/js-libp2p-websockets/issues/191)) ([e549691](https://github.com/libp2p/js-libp2p-websockets/commit/e549691e40577f9146355998cb504f071772e4e3)), closes [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#27](https://github.com/libp2p/js-libp2p-websockets/issues/27) [#24](https://github.com/libp2p/js-libp2p-websockets/issues/24) +* **dev:** bump it-take from 1.0.2 to 2.0.0 ([#192](https://github.com/libp2p/js-libp2p-websockets/issues/192)) ([4c037fc](https://github.com/libp2p/js-libp2p-websockets/commit/4c037fc3c116a3ed2ec39aec3fed776fcb6c9690)), closes [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) + +## [5.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v4.0.1...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#190](https://github.com/libp2p/js-libp2p-websockets/issues/190)) ([388b30d](https://github.com/libp2p/js-libp2p-websockets/commit/388b30d1c1024e2f7fd9d8bea85701d997f59dbb)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v4.0.0...v4.0.1) (2022-10-07) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 4.0.3 to 6.0.1 ([#189](https://github.com/libp2p/js-libp2p-websockets/issues/189)) ([00b33f0](https://github.com/libp2p/js-libp2p-websockets/commit/00b33f07a9af8446dcf94a4a0567994f6deefcbf)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.4...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/interface-transport from 1.0.4 to 2.0.0 (#187) + +### Dependencies + +* bump @libp2p/interface-transport from 1.0.4 to 2.0.0 ([#187](https://github.com/libp2p/js-libp2p-websockets/issues/187)) ([bfeaf1b](https://github.com/libp2p/js-libp2p-websockets/commit/bfeaf1bc695c2becff8c47839726f8105269ad9c)) + +## [3.0.4](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.3...v3.0.4) (2022-09-21) + + +### Bug Fixes + +* remove set timeout ([#182](https://github.com/libp2p/js-libp2p-websockets/issues/182)) ([23518b0](https://github.com/libp2p/js-libp2p-websockets/commit/23518b0dad79d2c38bca8d600bd763703534b7a6)), closes [#121](https://github.com/libp2p/js-libp2p-websockets/issues/121) +* socket close event not working in browser ([#183](https://github.com/libp2p/js-libp2p-websockets/issues/183)) ([9076b5b](https://github.com/libp2p/js-libp2p-websockets/commit/9076b5bade8dd453b98fc73f0dc0bddaba0fe882)), closes [#179](https://github.com/libp2p/js-libp2p-websockets/issues/179) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([64411ee](https://github.com/libp2p/js-libp2p-websockets/commit/64411eef588a57adb65868940e489f0badfb579d)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#185](https://github.com/libp2p/js-libp2p-websockets/issues/185)) ([539db88](https://github.com/libp2p/js-libp2p-websockets/commit/539db8806dc7748f9c5d6c8ba785a3c78bb92c62)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.2...v3.0.3) (2022-09-01) + + +### Trivial Changes + +* update project config ([#180](https://github.com/libp2p/js-libp2p-websockets/issues/180)) ([4f79f9c](https://github.com/libp2p/js-libp2p-websockets/commit/4f79f9ce789a566b99c57597d2d71e2bce40fd6e)) + + +### Dependencies + +* **dev:** bump wherearewe from 1.0.2 to 2.0.1 ([#177](https://github.com/libp2p/js-libp2p-websockets/issues/177)) ([5d7ae6a](https://github.com/libp2p/js-libp2p-websockets/commit/5d7ae6a5c22c57e7f47f32405fd57ece98664e4d)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.1...v3.0.2) (2022-08-10) + + +### Bug Fixes + +* update all deps ([#176](https://github.com/libp2p/js-libp2p-websockets/issues/176)) ([4825cd7](https://github.com/libp2p/js-libp2p-websockets/commit/4825cd7c5cec0cfc495b8b4286658927779bebdc)) +* update dial function return type to avoid Connection import issue ([#171](https://github.com/libp2p/js-libp2p-websockets/issues/171)) ([7ea9f83](https://github.com/libp2p/js-libp2p-websockets/commit/7ea9f83c0e4c3b42ba6e16e3dee1932ddce340f6)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.0...v3.0.1) (2022-07-12) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 2.1.0 to 3.0.1 ([#168](https://github.com/libp2p/js-libp2p-websockets/issues/168)) ([8a17ed7](https://github.com/libp2p/js-libp2p-websockets/commit/8a17ed7eb70e7ac90053cd591bb1e6f331915341)) +* **deps:** bump @libp2p/utils from 2.0.1 to 3.0.0 ([#167](https://github.com/libp2p/js-libp2p-websockets/issues/167)) ([53ba721](https://github.com/libp2p/js-libp2p-websockets/commit/53ba7218d19068e2a6b038ecbea65993af7bd745)) +* update websockets import var ([#165](https://github.com/libp2p/js-libp2p-websockets/issues/165)) ([838b69e](https://github.com/libp2p/js-libp2p-websockets/commit/838b69e04d435e55d038e49f2df66322d986a2e3)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v2.0.1...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* the connection API has changed + +### Trivial Changes + +* **deps:** bump @libp2p/logger from 1.1.6 to 2.0.0 ([#160](https://github.com/libp2p/js-libp2p-websockets/issues/160)) ([9074c4a](https://github.com/libp2p/js-libp2p-websockets/commit/9074c4a6725b750a3f8c602aa2655c095d83973d)) +* update deps ([#164](https://github.com/libp2p/js-libp2p-websockets/issues/164)) ([d474a81](https://github.com/libp2p/js-libp2p-websockets/commit/d474a8184a0eec4c09c2ced5dd5d6314be536fb3)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v2.0.0...v2.0.1) (2022-06-16) + + +### Trivial Changes + +* **deps:** bump @libp2p/utils from 1.0.10 to 2.0.0 ([#161](https://github.com/libp2p/js-libp2p-websockets/issues/161)) ([39980fc](https://github.com/libp2p/js-libp2p-websockets/commit/39980fc7fe994a341fd7f2e8a63738a58cfd1b02)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.9...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#159](https://github.com/libp2p/js-libp2p-websockets/issues/159)) ([e140bed](https://github.com/libp2p/js-libp2p-websockets/commit/e140bed0ae98af8ef0f7b3d6ec2388fa6273e590)) + + +### Trivial Changes + +* increase timeout for firefox ([5098e19](https://github.com/libp2p/js-libp2p-websockets/commit/5098e19796975e29ec91b62f28b52797dc1defde)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.8...v1.0.9) (2022-05-23) + + +### Bug Fixes + +* update interfaces and use static string for toStringTag ([#157](https://github.com/libp2p/js-libp2p-websockets/issues/157)) ([0c93585](https://github.com/libp2p/js-libp2p-websockets/commit/0c93585d0d5cb67c15ba0046b68aa3b196290e12)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.7...v1.0.8) (2022-05-06) + + +### Bug Fixes + +* hard code tag ([#154](https://github.com/libp2p/js-libp2p-websockets/issues/154)) ([c36aebb](https://github.com/libp2p/js-libp2p-websockets/commit/c36aebb9a38434c3e2127b9251427aba53bbb09f)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.6...v1.0.7) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#153](https://github.com/libp2p/js-libp2p-websockets/issues/153)) ([57c5887](https://github.com/libp2p/js-libp2p-websockets/commit/57c588716627270bbc42ee5e5c4249b99b9af5e5)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.5...v1.0.6) (2022-04-11) + + +### Bug Fixes + +* remove entrypoint config ([#152](https://github.com/libp2p/js-libp2p-websockets/issues/152)) ([cf2334e](https://github.com/libp2p/js-libp2p-websockets/commit/cf2334e8f5063dc98d34776b81e3dad13e761e6e)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.4...v1.0.5) (2022-04-09) + + +### Trivial Changes + +* update tsconfig ([#151](https://github.com/libp2p/js-libp2p-websockets/issues/151)) ([c54d349](https://github.com/libp2p/js-libp2p-websockets/commit/c54d3495d8bc53eaa1e1f4c99d9da404652f5a8d)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.3...v1.0.4) (2022-04-08) + + +### Trivial Changes + +* update aegir ([#150](https://github.com/libp2p/js-libp2p-websockets/issues/150)) ([6c08294](https://github.com/libp2p/js-libp2p-websockets/commit/6c08294e98807e789b791286931d120cfef679cd)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.2...v1.0.3) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#146](https://github.com/libp2p/js-libp2p-websockets/issues/146)) ([26ef08b](https://github.com/libp2p/js-libp2p-websockets/commit/26ef08bd243ddf714a32acdb6e2a7392209af355)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.1...v1.0.2) (2022-02-21) + + +### Bug Fixes + +* update interfaces ([#145](https://github.com/libp2p/js-libp2p-websockets/issues/145)) ([213ebc5](https://github.com/libp2p/js-libp2p-websockets/commit/213ebc5f85c749d712e1441b5fe49dc636e25f64)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.0...v1.0.1) (2022-02-16) + + +### Bug Fixes + +* add toStringTag and export filters ([#142](https://github.com/libp2p/js-libp2p-websockets/issues/142)) ([03fd000](https://github.com/libp2p/js-libp2p-websockets/commit/03fd000088ac78ea25f8cdf123fbbe8923257ca4)) +* update typesversions ([1cfbc28](https://github.com/libp2p/js-libp2p-websockets/commit/1cfbc28f93adecb1a9b60f53ef5815c87d00c93c)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.16.2...v1.0.0) (2022-02-10) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#76](https://github.com/libp2p/js-libp2p-websockets/issues/76)) ([#140](https://github.com/libp2p/js-libp2p-websockets/issues/140)) ([c4f6508](https://github.com/libp2p/js-libp2p-websockets/commit/c4f65082a97def50524e56231ce6c84eddf99521)), closes [#139](https://github.com/libp2p/js-libp2p-websockets/issues/139) + +## [0.16.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.16.1...v0.16.2) (2021-09-28) + + + +## [0.16.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.16.0...v0.16.1) (2021-07-08) + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.9...v0.16.0) (2021-07-07) + + +### chore + +* update deps ([#134](https://github.com/libp2p/js-libp2p-websockets/issues/134)) ([27f6c41](https://github.com/libp2p/js-libp2p-websockets/commit/27f6c4175bd6d5ea3e727a9a6e43136c806077cc)) + + +### BREAKING CHANGES + +* uses new major of mafmt, multiaddr, etc + + + +## [0.15.9](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.8...v0.15.9) (2021-06-11) + + + +## [0.15.8](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.7...v0.15.8) (2021-06-08) + + +### Bug Fixes + +* listener get addrs with wss ([#130](https://github.com/libp2p/js-libp2p-websockets/issues/130)) ([ee47570](https://github.com/libp2p/js-libp2p-websockets/commit/ee47570ff79a51b8f3c3414934d5f7ab9d00f74d)) + + + +## [0.15.7](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.6...v0.15.7) (2021-05-04) + + + +## [0.15.6](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.5...v0.15.6) (2021-04-18) + + + +## [0.15.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.4...v0.15.5) (2021-04-12) + + + +## [0.15.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.3...v0.15.4) (2021-03-31) + + + +## [0.15.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.2...v0.15.3) (2021-02-22) + + + +## [0.15.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.1...v0.15.2) (2021-02-09) + + +### Bug Fixes + +* add error event handler ([#118](https://github.com/libp2p/js-libp2p-websockets/issues/118)) ([577d350](https://github.com/libp2p/js-libp2p-websockets/commit/577d3505f559b153ec9e0bbca7d31d2f164712bc)) + + + +## [0.15.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.0...v0.15.1) (2021-02-05) + + +### Bug Fixes + +* incompatibility with @evanw/esbuild[#740](https://github.com/libp2p/js-libp2p-websockets/issues/740) ([#120](https://github.com/libp2p/js-libp2p-websockets/issues/120)) ([96244f0](https://github.com/libp2p/js-libp2p-websockets/commit/96244f048929c5225905327ae27a88961fe535f8)) + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.1...v0.15.0) (2020-11-24) + + +### Bug Fixes + +* add buffer ([#112](https://github.com/libp2p/js-libp2p-websockets/issues/112)) ([8065e07](https://github.com/libp2p/js-libp2p-websockets/commit/8065e07bad57b5732cdcec5ce3829ac2361604cf)) +* catch thrown maConn errors in listener ([8bfb19a](https://github.com/libp2p/js-libp2p-websockets/commit/8bfb19a78f296c10d8e1a3c0ac608daa9ffcfefc)) +* remove use of assert module ([#101](https://github.com/libp2p/js-libp2p-websockets/issues/101)) ([89d3723](https://github.com/libp2p/js-libp2p-websockets/commit/89d37232b8f603804b6ce5cd8230cc75d2dd8e28)) +* replace node buffers with uint8arrays ([#115](https://github.com/libp2p/js-libp2p-websockets/issues/115)) ([a277bf6](https://github.com/libp2p/js-libp2p-websockets/commit/a277bf6bfbc7ad796e51f7646d7449c203384c06)) + + +### Features + +* custom address filter ([#116](https://github.com/libp2p/js-libp2p-websockets/issues/116)) ([711c721](https://github.com/libp2p/js-libp2p-websockets/commit/711c721b033d28b3c57c37bf9ca98d0f5d2a58b6)) + + +### BREAKING CHANGES + +* Only DNS+WSS addresses are now returned on filter by default in the browser. This can be overritten by the filter option and filters are provided in the module. + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.6...v0.14.0) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#115](https://github.com/libp2p/js-libp2p-websockets/issues/115)) ([a277bf6](https://github.com/libp2p/js-libp2p-websockets/commit/a277bf6)) + + +### BREAKING CHANGES + +* - All deps used by this module now use Uint8Arrays in place of Buffers + +* chore: remove gh dep + + + + +## [0.13.6](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.5...v0.13.6) (2020-03-23) + + +### Bug Fixes + +* add buffer ([#112](https://github.com/libp2p/js-libp2p-websockets/issues/112)) ([8065e07](https://github.com/libp2p/js-libp2p-websockets/commit/8065e07)) + + + + +## [0.13.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.4...v0.13.5) (2020-02-26) + + +### Bug Fixes + +* catch thrown maConn errors in listener ([8bfb19a](https://github.com/libp2p/js-libp2p-websockets/commit/8bfb19a)) + + + + +## [0.13.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.3...v0.13.4) (2020-02-14) + + +### Bug Fixes + +* remove use of assert module ([#101](https://github.com/libp2p/js-libp2p-websockets/issues/101)) ([89d3723](https://github.com/libp2p/js-libp2p-websockets/commit/89d3723)) + + + + +## [0.13.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.2...v0.13.3) (2020-02-07) + + + + +## [0.13.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.1...v0.13.2) (2019-12-20) + + + + +## [0.13.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.0...v0.13.1) (2019-10-30) + + +### Bug Fixes + +* catch inbound upgrade errors ([#96](https://github.com/libp2p/js-libp2p-websockets/issues/96)) ([5b59fc3](https://github.com/libp2p/js-libp2p-websockets/commit/5b59fc3)) +* support bufferlist usage ([#97](https://github.com/libp2p/js-libp2p-websockets/issues/97)) ([3bf66d0](https://github.com/libp2p/js-libp2p-websockets/commit/3bf66d0)) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.3...v0.13.0) (2019-09-30) + + +### Code Refactoring + +* async with multiaddr conn ([#92](https://github.com/libp2p/js-libp2p-websockets/issues/92)) ([ce7bf4f](https://github.com/libp2p/js-libp2p-websockets/commit/ce7bf4f)) + + +### BREAKING CHANGES + +* Switch to using async/await and async iterators. The transport and connection interfaces have changed. See the README for new usage. + + + + +## [0.12.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.2...v0.12.3) (2019-08-21) + + + + +## [0.12.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.1...v0.12.2) (2019-01-24) + + +### Bug Fixes + +* ipv6 naming with multiaddr-to-uri package ([#81](https://github.com/libp2p/js-libp2p-websockets/issues/81)) ([93ef7c3](https://github.com/libp2p/js-libp2p-websockets/commit/93ef7c3)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.0...v0.12.1) (2019-01-10) + + +### Bug Fixes + +* reduce bundle size ([68ae2c3](https://github.com/libp2p/js-libp2p-websockets/commit/68ae2c3)) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.11.0...v0.12.0) (2018-04-30) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.5...v0.11.0) (2018-04-05) + + +### Features + +* add class-is module ([#72](https://github.com/libp2p/js-libp2p-websockets/issues/72)) ([f59cf88](https://github.com/libp2p/js-libp2p-websockets/commit/f59cf88)) +* Pass options to websocket server ([#66](https://github.com/libp2p/js-libp2p-websockets/issues/66)) ([709989a](https://github.com/libp2p/js-libp2p-websockets/commit/709989a)) + + + + +## [0.10.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.4...v0.10.5) (2018-02-20) + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.2...v0.10.4) (2017-10-22) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.2...v0.10.3) (2017-10-22) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.1...v0.10.2) (2017-10-20) + + +### Features + +* filter IPFS addrs correctly ([#62](https://github.com/libp2p/js-libp2p-websockets/issues/62)) ([9ddff85](https://github.com/libp2p/js-libp2p-websockets/commit/9ddff85)), closes [#64](https://github.com/libp2p/js-libp2p-websockets/issues/64) +* new aegir ([3d3cdf1](https://github.com/libp2p/js-libp2p-websockets/commit/3d3cdf1)) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.0...v0.10.1) (2017-07-22) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.6...v0.10.0) (2017-03-27) + + +### Bug Fixes + +* **dial:** pass through errors from pull-ws onConnect ([8df8084](https://github.com/libp2p/js-libp2p-websockets/commit/8df8084)) + + + + +## [0.9.6](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.5...v0.9.6) (2017-03-23) + + +### Bug Fixes + +* address parsing ([#57](https://github.com/libp2p/js-libp2p-websockets/issues/57)) ([9fbbe3f](https://github.com/libp2p/js-libp2p-websockets/commit/9fbbe3f)) + + + + +## [0.9.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.4...v0.9.5) (2017-03-23) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.2...v0.9.4) (2017-03-21) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.1...v0.9.2) (2017-02-09) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.0...v0.9.1) (2016-11-08) + + +### Bug Fixes + +* onConnect does not follow callback pattern ([#36](https://github.com/libp2p/js-libp2p-websockets/issues/36)) ([a821c33](https://github.com/libp2p/js-libp2p-websockets/commit/a821c33)) +* the fix ([0429beb](https://github.com/libp2p/js-libp2p-websockets/commit/0429beb)) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.8.1...v0.9.0) (2016-11-03) + + +### Features + +* upgrade to aegir@9 ([#33](https://github.com/libp2p/js-libp2p-websockets/issues/33)) ([e73c99e](https://github.com/libp2p/js-libp2p-websockets/commit/e73c99e)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.8.0...v0.8.1) (2016-09-06) + + +### Features + +* **readme:** update pull-streams section ([64c57f5](https://github.com/libp2p/js-libp2p-websockets/commit/64c57f5)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.7.2...v0.8.0) (2016-09-06) + + +### Features + +* **pull:** migrate to pull streams ([3f58dca](https://github.com/libp2p/js-libp2p-websockets/commit/3f58dca)) +* **readme:** complete the readme, adding reference about pull-streams ([b62560e](https://github.com/libp2p/js-libp2p-websockets/commit/b62560e)) + + + + +## [0.7.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.7.1...v0.7.2) (2016-08-29) + + +### Bug Fixes + +* **style:** reduce nested callbacks ([33f5fb3](https://github.com/libp2p/js-libp2p-websockets/commit/33f5fb3)) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.7.0...v0.7.1) (2016-08-03) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.6.1...v0.7.0) (2016-06-22) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.6.0...v0.6.1) (2016-05-29) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.5.0...v0.6.0) (2016-05-22) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.4.4...v0.5.0) (2016-05-17) + + + + +## [0.4.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.4.3...v0.4.4) (2016-05-08) + + +### Bug Fixes + +* improve close handling ([cd89354](https://github.com/libp2p/js-libp2p-websockets/commit/cd89354)) + + + + +## [0.4.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.4.1...v0.4.3) (2016-05-08) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.3.2...v0.4.1) (2016-04-25) + + + + +## [0.3.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.2.2...v0.3.2) (2016-04-14) + + + + +## [0.2.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.2.1...v0.2.2) (2016-04-14) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.2.0...v0.2.1) (2016-03-20) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.1.0...v0.2.0) (2016-03-14) + + + + +# 0.1.0 (2016-02-26) diff --git a/packages/transport-websockets/LICENSE b/packages/transport-websockets/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-websockets/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-websockets/LICENSE-APACHE b/packages/transport-websockets/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-websockets/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-websockets/LICENSE-MIT b/packages/transport-websockets/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-websockets/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/transport-websockets/README.md b/packages/transport-websockets/README.md new file mode 100644 index 0000000000..1a6a894b79 --- /dev/null +++ b/packages/transport-websockets/README.md @@ -0,0 +1,130 @@ +# @libp2p/websockets + +[![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 the WebSockets module that libp2p uses and that implements the interface-transport spec + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## Usage + +```sh +> npm i @libp2p/websockets +``` + +### Constructor properties + +```js +import { createLibp2pNode } from 'libp2p' +import { webSockets } from '@libp2p/webrtc-direct' + +const node = await createLibp2p({ + transports: [ + webSockets() + ] + //... other config +}) +await node.start() +await node.dial('/ip4/127.0.0.1/tcp/9090/ws') +``` + +| Name | Type | Description | Default | +| -------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| upgrader | [`Upgrader`](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/transport#upgrader) | connection upgrader object with `upgradeOutbound` and `upgradeInbound` | **REQUIRED** | +| filter | `(multiaddrs: Array) => Array` | override transport addresses filter | **Browser:** DNS+WSS multiaddrs / **Node.js:** DNS+\[WS, WSS] multiaddrs | + +You can create your own address filters for this transports, or rely in the filters [provided](./src/filters.js). + +The available filters are: + +- `filters.all` + - Returns all TCP and DNS based addresses, both with `ws` or `wss`. +- `filters.dnsWss` + - Returns all DNS based addresses with `wss`. +- `filters.dnsWsOrWss` + - Returns all DNS based addresses, both with `ws` or `wss`. + +## Libp2p Usage Example + +```js +import { createLibp2pNode } from 'libp2p' +import { websockets } from '@libp2p/websockets' +import filters from '@libp2p/websockets/filters' +import { mplex } from '@libp2p/mplex' +import { noise } from '@libp2p/noise' + +const transportKey = Websockets.prototype[Symbol.toStringTag] +const node = await Libp2p.create({ + transport: [ + websockets({ + // connect to all sockets, even insecure ones + filters: filters.all + }) + ], + streamMuxers: [ + mplex() + ], + connectionEncryption: [ + noise() + ] +}) +``` + +For more information see [libp2p/js-libp2p/doc/CONFIGURATION.md#customizing-transports](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#customizing-transports). + +## API + +### Transport + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) + +### Connection + +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-websockets/package.json b/packages/transport-websockets/package.json new file mode 100644 index 0000000000..f6a6b8a18a --- /dev/null +++ b/packages/transport-websockets/package.json @@ -0,0 +1,107 @@ +{ + "name": "@libp2p/websockets", + "version": "6.0.3", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/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": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser -f ./dist/test/browser.js --cov", + "test:chrome-webworker": "aegir test -t webworker -f ./dist/test/browser.js", + "test:firefox": "aegir test -t browser -f ./dist/test/browser.js -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -f ./dist/test/browser.js -- --browser firefox", + "test:node": "aegir test -t node -f ./dist/test/node.js --cov", + "test:electron-main": "aegir test -t electron-main -f ./dist/test/node.js --cov" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", + "@multiformats/multiaddr-to-uri": "^9.0.2", + "@types/ws": "^8.5.4", + "abortable-iterator": "^5.0.1", + "it-ws": "^6.0.0", + "p-defer": "^4.0.0", + "p-timeout": "^6.0.0", + "wherearewe": "^2.0.1", + "ws": "^8.12.1" + }, + "devDependencies": { + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/interface-transport-compliance-tests": "^4.0.0", + "aegir": "^39.0.10", + "is-loopback-addr": "^2.0.1", + "it-all": "^3.0.1", + "it-drain": "^3.0.2", + "it-goodbye": "^4.0.1", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "p-wait-for": "^5.0.0", + "uint8arraylist": "^2.3.2", + "uint8arrays": "^4.0.3" + }, + "browser": { + "./dist/src/listener.js": "./dist/src/listener.browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-websockets/src/constants.ts b/packages/transport-websockets/src/constants.ts new file mode 100644 index 0000000000..e8c3939e1d --- /dev/null +++ b/packages/transport-websockets/src/constants.ts @@ -0,0 +1,10 @@ +// p2p multi-address code +export const CODE_P2P = 421 +export const CODE_CIRCUIT = 290 + +export const CODE_TCP = 6 +export const CODE_WS = 477 +export const CODE_WSS = 478 + +// Time to wait for a connection to close gracefully before destroying it manually +export const CLOSE_TIMEOUT = 2000 diff --git a/packages/transport-websockets/src/filters.ts b/packages/transport-websockets/src/filters.ts new file mode 100644 index 0000000000..8536e7c97a --- /dev/null +++ b/packages/transport-websockets/src/filters.ts @@ -0,0 +1,66 @@ +import * as mafmt from '@multiformats/mafmt' +import { + CODE_CIRCUIT, + CODE_P2P, + CODE_TCP, + CODE_WS, + CODE_WSS +} from './constants.js' +import type { Multiaddr } from '@multiformats/multiaddr' + +export function all (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSockets.matches(testMa) || + mafmt.WebSocketsSecure.matches(testMa) + }) +} + +export function wss (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSocketsSecure.matches(testMa) + }) +} + +export function dnsWss (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSocketsSecure.matches(testMa) && + mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) + }) +} + +export function dnsWsOrWss (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + // WS + if (mafmt.WebSockets.matches(testMa)) { + return mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WS)) + } + + // WSS + return mafmt.WebSocketsSecure.matches(testMa) && + mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) + }) +} diff --git a/packages/transport-websockets/src/index.ts b/packages/transport-websockets/src/index.ts new file mode 100644 index 0000000000..0ce23bd7e8 --- /dev/null +++ b/packages/transport-websockets/src/index.ts @@ -0,0 +1,143 @@ +import { type Transport, type MultiaddrFilter, symbol, type CreateListenerOptions, type DialOptions, type Listener } from '@libp2p/interface-transport' +import { AbortError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri' +import { connect, type WebSocketOptions } from 'it-ws/client' +import pDefer from 'p-defer' +import { isBrowser, isWebWorker } from 'wherearewe' +import * as filters from './filters.js' +import { createListener } from './listener.js' +import { socketToMaConn } from './socket-to-conn.js' +import type { Connection } from '@libp2p/interface-connection' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Server } from 'http' +import type { DuplexWebSocket } from 'it-ws/duplex' +import type { ClientOptions } from 'ws' + +const log = logger('libp2p:websockets') + +export interface WebSocketsInit extends AbortOptions, WebSocketOptions { + filter?: MultiaddrFilter + websocket?: ClientOptions + server?: Server +} + +class WebSockets implements Transport { + private readonly init?: WebSocketsInit + + constructor (init?: WebSocketsInit) { + this.init = init + } + + readonly [Symbol.toStringTag] = '@libp2p/websockets' + + readonly [symbol] = true + + async dial (ma: Multiaddr, options: DialOptions): Promise { + log('dialing %s', ma) + options = options ?? {} + + const socket = await this._connect(ma, options) + const maConn = socketToMaConn(socket, ma) + log('new outbound connection %s', maConn.remoteAddr) + + const conn = await options.upgrader.upgradeOutbound(maConn) + log('outbound connection %s upgraded', maConn.remoteAddr) + return conn + } + + async _connect (ma: Multiaddr, options: AbortOptions): Promise { + if (options?.signal?.aborted === true) { + throw new AbortError() + } + const cOpts = ma.toOptions() + log('dialing %s:%s', cOpts.host, cOpts.port) + + const errorPromise = pDefer() + const errfn = (err: any): void => { + log.error('connection error:', err) + + errorPromise.reject(err) + } + + const rawSocket = connect(toUri(ma), this.init) + + if (rawSocket.socket.on != null) { + rawSocket.socket.on('error', errfn) + } else { + rawSocket.socket.onerror = errfn + } + + if (options.signal == null) { + await Promise.race([rawSocket.connected(), errorPromise.promise]) + + log('connected %s', ma) + return rawSocket + } + + // Allow abort via signal during connect + let onAbort + const abort = new Promise((resolve, reject) => { + onAbort = () => { + reject(new AbortError()) + rawSocket.close().catch(err => { + log.error('error closing raw socket', err) + }) + } + + // Already aborted? + if (options?.signal?.aborted === true) { + onAbort(); return + } + + options?.signal?.addEventListener('abort', onAbort) + }) + + try { + await Promise.race([abort, errorPromise.promise, rawSocket.connected()]) + } finally { + if (onAbort != null) { + options?.signal?.removeEventListener('abort', onAbort) + } + } + + log('connected %s', ma) + return rawSocket + } + + /** + * Creates a Websockets listener. The provided `handler` function will be called + * anytime a new incoming Connection has been successfully upgraded via + * `upgrader.upgradeInbound` + */ + createListener (options: CreateListenerOptions): Listener { + return createListener({ ...this.init, ...options }) + } + + /** + * Takes a list of `Multiaddr`s and returns only valid Websockets addresses. + * By default, in a browser environment only DNS+WSS multiaddr is accepted, + * while in a Node.js environment DNS+{WS, WSS} multiaddrs are accepted. + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + if (this.init?.filter != null) { + return this.init?.filter(multiaddrs) + } + + // Browser + if (isBrowser || isWebWorker) { + return filters.wss(multiaddrs) + } + + return filters.all(multiaddrs) + } +} + +export function webSockets (init: WebSocketsInit = {}): (components?: any) => Transport { + return () => { + return new WebSockets(init) + } +} diff --git a/packages/transport-websockets/src/listener.browser.ts b/packages/transport-websockets/src/listener.browser.ts new file mode 100644 index 0000000000..d5568a9d8a --- /dev/null +++ b/packages/transport-websockets/src/listener.browser.ts @@ -0,0 +1,5 @@ +import type { Listener } from '@libp2p/interface-transport' + +export function createListener (): Listener { + throw new Error('WebSocket Servers can not be created in the browser!') +} diff --git a/packages/transport-websockets/src/listener.ts b/packages/transport-websockets/src/listener.ts new file mode 100644 index 0000000000..abb63aa894 --- /dev/null +++ b/packages/transport-websockets/src/listener.ts @@ -0,0 +1,160 @@ +import os from 'os' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr' +import { multiaddr, protocols } from '@multiformats/multiaddr' +import { createServer } from 'it-ws/server' +import { socketToMaConn } from './socket-to-conn.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Listener, ListenerEvents, CreateListenerOptions } from '@libp2p/interface-transport' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Server } from 'http' +import type { DuplexWebSocket } from 'it-ws/duplex' +import type { WebSocketServer } from 'it-ws/server' + +const log = logger('libp2p:websockets:listener') + +class WebSocketListener extends EventEmitter implements Listener { + private readonly connections: Set + private listeningMultiaddr?: Multiaddr + private readonly server: WebSocketServer + + constructor (init: WebSocketListenerInit) { + super() + + // Keep track of open connections to destroy when the listener is closed + this.connections = new Set() + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + this.server = createServer({ + ...init, + onConnection: (stream: DuplexWebSocket) => { + const maConn = socketToMaConn(stream, toMultiaddr(stream.remoteAddress ?? '', stream.remotePort ?? 0)) + log('new inbound connection %s', maConn.remoteAddr) + + this.connections.add(stream) + + stream.socket.on('close', function () { + self.connections.delete(stream) + }) + + try { + void init.upgrader.upgradeInbound(maConn) + .then((conn) => { + log('inbound connection %s upgraded', maConn.remoteAddr) + + if (init?.handler != null) { + init?.handler(conn) + } + + self.dispatchEvent(new CustomEvent('connection', { + detail: conn + })) + }) + .catch(async err => { + log.error('inbound connection failed to upgrade', err) + + await maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) + }) + } catch (err) { + log.error('inbound connection failed to upgrade', err) + maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) + } + } + }) + + this.server.on('listening', () => { + this.dispatchEvent(new CustomEvent('listening')) + }) + this.server.on('error', (err: Error) => { + this.dispatchEvent(new CustomEvent('error', { + detail: err + })) + }) + this.server.on('close', () => { + this.dispatchEvent(new CustomEvent('close')) + }) + } + + async close (): Promise { + await Promise.all( + Array.from(this.connections).map(async maConn => { await maConn.close() }) + ) + + if (this.server.address() == null) { + // not listening, close will throw an error + return + } + + await this.server.close() + } + + async listen (ma: Multiaddr): Promise { + this.listeningMultiaddr = ma + + await this.server.listen(ma.toOptions()) + } + + getAddrs (): Multiaddr[] { + const multiaddrs = [] + const address = this.server.address() + + if (address == null) { + throw new Error('Listener is not ready yet') + } + + if (typeof address === 'string') { + throw new Error('Wrong address type received - expected AddressInfo, got string - are you trying to listen on a unix socket?') + } + + if (this.listeningMultiaddr == null) { + throw new Error('Listener is not ready yet') + } + + const ipfsId = this.listeningMultiaddr.getPeerId() + const protos = this.listeningMultiaddr.protos() + + // Because TCP will only return the IPv6 version + // we need to capture from the passed multiaddr + if (protos.some(proto => proto.code === protocols('ip4').code)) { + const wsProto = protos.some(proto => proto.code === protocols('ws').code) ? '/ws' : '/wss' + let m = this.listeningMultiaddr.decapsulate('tcp') + m = m.encapsulate(`/tcp/${address.port}${wsProto}`) + if (ipfsId != null) { + m = m.encapsulate(`/p2p/${ipfsId}`) + } + + if (m.toString().includes('0.0.0.0')) { + const netInterfaces = os.networkInterfaces() + Object.values(netInterfaces).forEach(niInfos => { + if (niInfos == null) { + return + } + + niInfos.forEach(ni => { + if (ni.family === 'IPv4') { + multiaddrs.push(multiaddr(m.toString().replace('0.0.0.0', ni.address))) + } + }) + }) + } else { + multiaddrs.push(m) + } + } + + return multiaddrs + } +} + +export interface WebSocketListenerInit extends CreateListenerOptions { + server?: Server +} + +export function createListener (init: WebSocketListenerInit): Listener { + return new WebSocketListener(init) +} diff --git a/packages/transport-websockets/src/socket-to-conn.ts b/packages/transport-websockets/src/socket-to-conn.ts new file mode 100644 index 0000000000..a3bf2b8699 --- /dev/null +++ b/packages/transport-websockets/src/socket-to-conn.ts @@ -0,0 +1,71 @@ +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import pTimeout from 'p-timeout' +import { CLOSE_TIMEOUT } from './constants.js' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { DuplexWebSocket } from 'it-ws/duplex' + +const log = logger('libp2p:websockets:socket') + +export interface SocketToConnOptions extends AbortOptions { + localAddr?: Multiaddr +} + +// Convert a stream into a MultiaddrConnection +// https://github.com/libp2p/interface-transport#multiaddrconnection +export function socketToMaConn (stream: DuplexWebSocket, remoteAddr: Multiaddr, options?: SocketToConnOptions): MultiaddrConnection { + options = options ?? {} + + const maConn: MultiaddrConnection = { + async sink (source) { + if ((options?.signal) != null) { + source = abortableSource(source, options.signal) + } + + try { + await stream.sink(source) + } catch (err: any) { + if (err.type !== 'aborted') { + log.error(err) + } + } + }, + + source: (options.signal != null) ? abortableSource(stream.source, options.signal) : stream.source, + + remoteAddr, + + timeline: { open: Date.now() }, + + async close () { + const start = Date.now() + + try { + await pTimeout(stream.close(), { + milliseconds: CLOSE_TIMEOUT + }) + } catch (err) { + const { host, port } = maConn.remoteAddr.toOptions() + log('timeout closing stream to %s:%s after %dms, destroying it manually', + host, port, Date.now() - start) + + stream.destroy() + } finally { + maConn.timeline.close = Date.now() + } + } + } + + stream.socket.addEventListener('close', () => { + // In instances where `close` was not explicitly called, + // such as an iterable stream ending, ensure we have set the close + // timeline + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + }, { once: true }) + + return maConn +} diff --git a/packages/transport-websockets/test/browser.ts b/packages/transport-websockets/test/browser.ts new file mode 100644 index 0000000000..5fc89b6f89 --- /dev/null +++ b/packages/transport-websockets/test/browser.ts @@ -0,0 +1,98 @@ +/* eslint-env mocha */ + +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pipe } from 'it-pipe' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { isBrowser, isWebWorker } from 'wherearewe' +import { webSockets } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Transport } from '@libp2p/interface-transport' + +const protocol = '/echo/1.0.0' + +describe('libp2p-websockets', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9095/ws') + let ws: Transport + let conn: Connection + + beforeEach(async () => { + ws = webSockets()() + conn = await ws.dial(ma, { + upgrader: mockUpgrader({ + events: new EventEmitter() + }) + }) + }) + + afterEach(async () => { + await conn.close() + }) + + it('echo', async () => { + const data = uint8ArrayFromString('hey') + const stream = await conn.newStream([protocol]) + + const res = await pipe( + [data], + stream, + async (source) => all(source) + ) + + expect(res[0].subarray()).to.equalBytes(data) + }) + + it('should filter out no wss websocket addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/443/wss') + const ma3 = multiaddr('/ip6/::1/tcp/80/ws') + const ma4 = multiaddr('/ip6/::1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + + if (isBrowser || isWebWorker) { + expect(valid.length).to.equal(2) + expect(valid).to.deep.equal([ma2, ma4]) + } else { + expect(valid.length).to.equal(4) + } + }) + + describe('stress', () => { + it('one big write', async () => { + const data = new Uint8Array(1000000).fill(5) + const stream = await conn.newStream([protocol]) + + const res = await pipe( + [data], + stream, + async (source) => all(source) + ) + + expect(res[0].subarray()).to.deep.equal(data) + }) + + it('many writes', async function () { + this.timeout(60000) + + const count = 20000 + const data = Array(count).fill(0).map(() => uint8ArrayFromString(Math.random().toString())) + const stream = await conn.newStream([protocol]) + + const res = await pipe( + data, + stream, + async (source) => all(source) + ) + + expect(res.map(list => list.subarray())).to.deep.equal(data) + }) + }) + + it('.createServer throws in browser', () => { + expect(webSockets()().createListener).to.throw() + }) +}) diff --git a/packages/transport-websockets/test/compliance.node.ts b/packages/transport-websockets/test/compliance.node.ts new file mode 100644 index 0000000000..eef82737de --- /dev/null +++ b/packages/transport-websockets/test/compliance.node.ts @@ -0,0 +1,60 @@ +/* eslint-env mocha */ + +import http from 'http' +import tests from '@libp2p/interface-transport-compliance-tests' +import { multiaddr } from '@multiformats/multiaddr' +import * as filters from '../src/filters.js' +import { webSockets } from '../src/index.js' +import type { WebSocketListenerInit } from '../src/listener.js' +import type { Listener } from '@libp2p/interface-transport' + +describe('interface-transport compliance', () => { + tests({ + async setup () { + const ws = webSockets({ filter: filters.all })() + const addrs = [ + multiaddr('/ip4/127.0.0.1/tcp/9091/ws'), + multiaddr('/ip4/127.0.0.1/tcp/9092/ws'), + multiaddr('/dns4/ipfs.io/tcp/9092/ws'), + multiaddr('/dns4/ipfs.io/tcp/9092/wss') + ] + + let delayMs = 0 + const delayedCreateListener = (options: WebSocketListenerInit): Listener => { + // A server that will delay the upgrade event by delayMs + options.server = new Proxy(http.createServer(), { + get (server, prop) { + if (prop === 'on') { + return (event: string, handler: (...args: any[]) => void) => { + server.on(event, (...args) => { + if (event !== 'upgrade' || delayMs === 0) { + handler(...args); return + } + setTimeout(() => { handler(...args) }, delayMs) + }) + } + } + // @ts-expect-error cannot access props with a string + return server[prop] + } + }) + + return ws.createListener(options) + } + + const wsProxy = new Proxy(ws, { + // @ts-expect-error cannot access props with a string + get: (_, prop) => prop === 'createListener' ? delayedCreateListener : ws[prop] + }) + + // Used by the dial tests to simulate a delayed connect + const connector = { + delay (ms: number) { delayMs = ms }, + restore () { delayMs = 0 } + } + + return { transport: wsProxy, addrs, connector } + }, + async teardown () {} + }) +}) diff --git a/packages/transport-websockets/test/fixtures/certificate.pem b/packages/transport-websockets/test/fixtures/certificate.pem new file mode 100644 index 0000000000..840776c01c --- /dev/null +++ b/packages/transport-websockets/test/fixtures/certificate.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICATCCAWoCCQDPufXH86n2QzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJu +bzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEyMDEwMTE0NDQwMFoXDTIwMDMxOTE0NDQwMFowRTELMAkG +A1UEBhMCbm8xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtrQ7 ++r//2iV/B6F+4boH0XqFn7alcV9lpjvAmwRXNKnxAoa0f97AjYPGNLKrjpkNXXhB +JROIdbRbZnCNeC5fzX1a+JCo7KStzBXuGSZr27TtFmcV4H+9gIRIcNHtZmJLnxbJ +sIhkGR8yVYdmJZe4eT5ldk1zoB1adgPF1hZhCBMCAwEAATANBgkqhkiG9w0BAQUF +AAOBgQCeWBEHYJ4mCB5McwSSUox0T+/mJ4W48L/ZUE4LtRhHasU9hiW92xZkTa7E +QLcoJKQiWfiLX2ysAro0NX4+V8iqLziMqvswnPzz5nezaOLE/9U/QvH3l8qqNkXu +rNbsW1h/IO6FV8avWFYVFoutUwOaZ809k7iMh2F2JMgXQ5EymQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/packages/transport-websockets/test/fixtures/key.pem b/packages/transport-websockets/test/fixtures/key.pem new file mode 100644 index 0000000000..3649a93301 --- /dev/null +++ b/packages/transport-websockets/test/fixtures/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ +3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg +f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB +AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l +3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk +Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs +ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq +DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL +Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX +OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav +AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ +yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 +JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/packages/transport-websockets/test/node.ts b/packages/transport-websockets/test/node.ts new file mode 100644 index 0000000000..43d88d5d7a --- /dev/null +++ b/packages/transport-websockets/test/node.ts @@ -0,0 +1,639 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import fs from 'fs' +import http from 'http' +import https from 'https' +import { mockRegistrar, mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { isLoopbackAddr } from 'is-loopback-addr' +import all from 'it-all' +import drain from 'it-drain' +import { goodbye } from 'it-goodbye' +import { pipe } from 'it-pipe' +import defer from 'p-defer' +import waitFor from 'p-wait-for' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as filters from '../src/filters.js' +import { webSockets } from '../src/index.js' +import type { Listener, Transport } from '@libp2p/interface-transport' +import type { Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' +import './compliance.node.js' + +async function * toBuffers (source: Source): AsyncGenerator { + for await (const list of source) { + yield * list + } +} + +const protocol = '/say-hello/1.0.0' +const registrar = mockRegistrar() +void registrar.handle(protocol, (evt) => { + void pipe([ + uint8ArrayFromString('hey') + ], + evt.stream, + drain + ) +}) +const upgrader = mockUpgrader({ + registrar, + events: new EventEmitter() +}) + +describe('instantiate the transport', () => { + it('create', () => { + const ws = webSockets()() + expect(ws).to.exist() + }) +}) + +describe('listen', () => { + it('should close connections when stopping the listener', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws') + + const ws = webSockets()() + const listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + + const conn = await ws.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + void pipe(stream, stream) + + await listener.close() + + await waitFor(() => conn.stat.timeline.close != null) + }) + + describe('ip4', () => { + let ws: Transport + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws') + let listener: Listener + + beforeEach(() => { + ws = webSockets()() + }) + + afterEach(async () => { + await listener.close() + }) + + it('listen, check for promise', async () => { + listener = ws.createListener({ upgrader }) + await listener.listen(ma) + }) + + it('listen, check for listening event', (done) => { + listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + done() + }) + + void listener.listen(ma) + }) + + it('should error on starting two listeners on same address', async () => { + listener = ws.createListener({ upgrader }) + const dumbServer = http.createServer() + await new Promise(resolve => dumbServer.listen(ma.toOptions().port, resolve)) + await expect(listener.listen(ma)).to.eventually.rejectedWith('listen EADDRINUSE') + await new Promise(resolve => dumbServer.close(() => { resolve() })) + }) + + it('listen, check for the close event', (done) => { + const listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + listener.addEventListener('close', () => { done() }) + void listener.close() + }) + + void listener.listen(ma) + }) + + it('listen on addr with /ipfs/QmHASH', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + }) + + it('listen on port 0', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/0/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().port)).to.not.include(0) + }) + + it('listen on any Interface', async () => { + const ma = multiaddr('/ip4/0.0.0.0/tcp/0/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') + }) + + it('getAddrs', async () => { + listener = ws.createListener({ upgrader }) + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.length).to.equal(1) + expect(addrs[0]).to.deep.equal(ma) + }) + + it('getAddrs on port 0 listen', async () => { + const addr = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + listener = ws.createListener({ upgrader }) + await listener.listen(addr) + const addrs = listener.getAddrs() + expect(addrs.length).to.equal(1) + expect(addrs.map((a) => a.toOptions().port)).to.not.include('0') + }) + + it('getAddrs from listening on 0.0.0.0', async () => { + const addr = multiaddr('/ip4/0.0.0.0/tcp/47382/ws') + listener = ws.createListener({ upgrader }) + await listener.listen(addr) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') + }) + + it('getAddrs from listening on 0.0.0.0 and port 0', async () => { + const addr = multiaddr('/ip4/0.0.0.0/tcp/0/ws') + listener = ws.createListener({ upgrader }) + await listener.listen(addr) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') + expect(addrs.map((a) => a.toOptions().port)).to.not.include('0') + }) + + it('getAddrs preserves p2p Id', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.length).to.equal(1) + expect(addrs[0]).to.deep.equal(ma) + }) + }) + + describe('ip6', () => { + let ws: Transport + const ma = multiaddr('/ip6/::1/tcp/9091/ws') + + beforeEach(() => { + ws = webSockets()() + }) + + it('listen, check for promise', async () => { + const listener = ws.createListener({ upgrader }) + await listener.listen(ma) + await listener.close() + }) + + it('listen, check for listening event', (done) => { + const listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + void listener.close().then(done, done) + }) + + void listener.listen(ma) + }) + + it('listen, check for the close event', (done) => { + const listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + listener.addEventListener('close', () => { done() }) + void listener.close() + }) + + void listener.listen(ma) + }) + + it('listen on addr with /ipfs/QmHASH', async () => { + const ma = multiaddr('/ip6/::1/tcp/9091/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const listener = ws.createListener({ upgrader }) + await listener.listen(ma) + await listener.close() + }) + }) +}) + +describe('dial', () => { + describe('ip4', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip4/127.0.0.1/tcp/9091/ws') + + beforeEach(async () => { + ws = webSockets()() + listener = ws.createListener({ upgrader }) + await listener.listen(ma) + }) + + afterEach(async () => { await listener.close() }) + + it('dial', async () => { + const conn = await ws.dial(ma, { upgrader }) + const stream = await conn.newStream([protocol]) + + expect((await all(stream.source)).map(list => list.subarray())).to.deep.equal([uint8ArrayFromString('hey')]) + await conn.close() + }) + + it('dial with p2p Id', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9091/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const conn = await ws.dial(ma, { upgrader }) + const stream = await conn.newStream([protocol]) + + expect((await all(stream.source)).map(list => list.subarray())).to.deep.equal([uint8ArrayFromString('hey')]) + await conn.close() + }) + + it('dial should throw on immediate abort', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + const controller = new AbortController() + + const conn = ws.dial(ma, { signal: controller.signal, upgrader }) + controller.abort() + + await expect(conn).to.eventually.be.rejected() + }) + + it('should resolve port 0', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + const ws = webSockets()() + + // Create a Promise that resolves when a connection is handled + const deferred = defer() + + const listener = ws.createListener({ handler: deferred.resolve, upgrader }) + + // Listen on the multiaddr + await listener.listen(ma) + + const localAddrs = listener.getAddrs() + expect(localAddrs.length).to.equal(1) + + // Dial to that address + await ws.dial(localAddrs[0], { upgrader }) + + // Wait for the incoming dial to be handled + await deferred.promise + + // close the listener + await listener.close() + }) + }) + + describe('ip4 no loopback', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip4/0.0.0.0/tcp/0/ws') + + beforeEach(async () => { + ws = webSockets()() + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + }) + + afterEach(async () => { await listener.close() }) + + it('dial', async () => { + const addrs = listener.getAddrs().filter((ma) => { + const { address } = ma.nodeAddress() + + return !isLoopbackAddr(address) + }) + + // Dial first no loopback address + const conn = await ws.dial(addrs[0], { upgrader }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const stream = await conn.newStream([protocol]) + + await expect(pipe( + s, + stream, + toBuffers, + s + )).to.eventually.deep.equal([uint8ArrayFromString('hey')]) + }) + }) + + describe('ip4 with wss', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip4/127.0.0.1/tcp/37284/wss') + let server: https.Server + + beforeEach(async () => { + server = https.createServer({ + cert: fs.readFileSync('./test/fixtures/certificate.pem'), + key: fs.readFileSync('./test/fixtures/key.pem') + }) + ws = webSockets({ websocket: { rejectUnauthorized: false }, server })() + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + }) + + afterEach(async () => { + await listener.close() + server.close() + }) + + it('should listen on wss address', () => { + const addrs = listener.getAddrs() + + expect(addrs).to.have.lengthOf(1) + expect(ma.equals(addrs[0])).to.eql(true) + }) + + it('dial ip4', async () => { + const conn = await ws.dial(ma, { upgrader }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const stream = await conn.newStream([protocol]) + + const res = await pipe(s, stream, toBuffers, s) + + expect(res[0]).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + }) + }) + + describe('ip6', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip6/::1/tcp/9091/ws') + + beforeEach(async () => { + ws = webSockets()() + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + }) + + afterEach(async () => { await listener.close() }) + + it('dial ip6', async () => { + const conn = await ws.dial(ma, { upgrader }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const stream = await conn.newStream([protocol]) + + await expect(pipe(s, stream, toBuffers, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) + }) + + it('dial with p2p Id', async () => { + const ma = multiaddr('/ip6/::1/tcp/9091/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const conn = await ws.dial(ma, { upgrader }) + + const s = goodbye({ + source: [uint8ArrayFromString('hey')], + sink: all + }) + const stream = await conn.newStream([protocol]) + + await expect(pipe(s, stream, toBuffers, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) + }) + }) +}) + +describe('filter addrs', () => { + let ws: Transport + + describe('default filter addrs with only dns', () => { + before(() => { + ws = webSockets()() + }) + + it('should filter out invalid WS addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma2 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma3 = multiaddr('/ip6/::1/tcp/80') + const ma4 = multiaddr('/dnsaddr/ipfs.io/tcp/80') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + expect(valid.length).to.equal(0) + }) + + it('should filter correct dns address', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/ws') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws') + const ma3 = multiaddr('/dnsaddr/ipfs.io/tcp/80/wss') + + const valid = ws.filter([ma1, ma2, ma3]) + expect(valid.length).to.equal(3) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + expect(valid[2]).to.deep.equal(ma3) + }) + + it('should filter correct dns address with ipfs id', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns4 address', function () { + const ma1 = multiaddr('/dns4/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns4/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address with ipfs id', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + }) + + describe('custom filter addrs', () => { + before(() => { + ws = webSockets()({ filter: filters.all }) + }) + + it('should fail invalid WS addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma2 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma3 = multiaddr('/ip6/::1/tcp/80') + const ma4 = multiaddr('/dnsaddr/ipfs.io/tcp/80') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + expect(valid.length).to.equal(0) + }) + + it('should filter correct ipv4 addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct ipv4 addresses with ipfs id', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/80/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct ipv6 address', function () { + const ma1 = multiaddr('/ip6/::1/tcp/80/ws') + const ma2 = multiaddr('/ip6/::1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct ipv6 addresses with ipfs id', function () { + const ma1 = multiaddr('/ip6/::1/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/ip6/::1/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns address', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/ws') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws') + const ma3 = multiaddr('/dnsaddr/ipfs.io/tcp/80/wss') + + const valid = ws.filter([ma1, ma2, ma3]) + expect(valid.length).to.equal(3) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + expect(valid[2]).to.deep.equal(ma3) + }) + + it('should filter correct dns address with ipfs id', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns4 address', function () { + const ma1 = multiaddr('/dns4/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns4/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address with ipfs id', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter mixed addresses', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma3 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma4 = multiaddr('/dns6/ipfs.io/ws') + const mh5 = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw' + + '/p2p-circuit/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2, ma3, ma4, mh5]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma4) + }) + + it('filter a single addr for this transport', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma]) + expect(valid.length).to.equal(1) + expect(valid[0]).to.deep.equal(ma) + }) + }) +}) diff --git a/packages/transport-websockets/tsconfig.json b/packages/transport-websockets/tsconfig.json new file mode 100644 index 0000000000..9047d208ae --- /dev/null +++ b/packages/transport-websockets/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-transport" + }, + { + "path": "../interface-transport-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../utils" + } + ] +} diff --git a/packages/transport-webtransport/.aegir.js b/packages/transport-webtransport/.aegir.js new file mode 100644 index 0000000000..a31c770f9b --- /dev/null +++ b/packages/transport-webtransport/.aegir.js @@ -0,0 +1,61 @@ +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' +import defer from 'p-defer' + +/** @type {import('aegir/types').PartialOptions} */ +export default { + test: { + async before() { + if (!existsSync('./go-libp2p-webtransport-server/main')) { + await new Promise((resolve, reject) => { + exec('go build -o main main.go', + { cwd: './go-libp2p-webtransport-server' }, + (error, stdout, stderr) => { + if (error) { + reject(error) + console.error(`exec error: ${error}`) + return + } + resolve() + }) + }) + } + + const server = spawn('./main', [], { cwd: './go-libp2p-webtransport-server', killSignal: 'SIGINT' }) + server.stderr.on('data', (data) => { + console.log('stderr:', data.toString()) + }) + const serverAddr = defer() + const serverAddr6 = defer() + + server.stdout.on('data', (buf) => { + const data = buf.toString() + + console.log('stdout:', data); + if (data.includes('addr=/ip4')) { + // Parse the addr out + serverAddr.resolve(`/ip4${data.match(/addr=\/ip4(.*)/)[1]}`) + } + + if (data.includes('addr=/ip6')) { + // Parse the addr out + serverAddr6.resolve(`/ip6${data.match(/addr=\/ip6(.*)/)[1]}`) + } + }) + + return { + server, + env: { + serverAddr: await serverAddr.promise, + serverAddr6: await serverAddr6.promise + } + } + }, + async after(_, { server }) { + server.kill('SIGINT') + } + }, + build: { + bundlesizeMax: '18kB' + } +} diff --git a/packages/transport-webtransport/.gitignore b/packages/transport-webtransport/.gitignore new file mode 100644 index 0000000000..431f7bde19 --- /dev/null +++ b/packages/transport-webtransport/.gitignore @@ -0,0 +1 @@ +go-libp2p-webtransport-server/main diff --git a/packages/transport-webtransport/CHANGELOG.md b/packages/transport-webtransport/CHANGELOG.md new file mode 100644 index 0000000000..562095f662 --- /dev/null +++ b/packages/transport-webtransport/CHANGELOG.md @@ -0,0 +1,142 @@ +## [2.0.2](https://github.com/libp2p/js-libp2p-webtransport/compare/v2.0.1...v2.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([56ef477](https://github.com/libp2p/js-libp2p-webtransport/commit/56ef477cff1214bebb150414ad6db36174ee0fa1)) +* Update .github/workflows/stale.yml [skip ci] ([cdbdfd4](https://github.com/libp2p/js-libp2p-webtransport/commit/cdbdfd4dc50a5e2bd3729938786a427ae7802f75)) + + +### Dependencies + +* 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)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-webtransport/compare/v2.0.0...v2.0.1) (2023-04-28) + + +### Dependencies + +* bump @libp2p/interface-transport from 2.1.3 to 4.0.1 ([#72](https://github.com/libp2p/js-libp2p-webtransport/issues/72)) ([04b977d](https://github.com/libp2p/js-libp2p-webtransport/commit/04b977db00bf0d71574f4618eaf0f070a5ce9441)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.11...v2.0.0) (2023-04-28) + + +### ⚠ BREAKING CHANGES + +* the type of the source/sink properties have changed + +### Dependencies + +* update stream types ([#66](https://github.com/libp2p/js-libp2p-webtransport/issues/66)) ([3772060](https://github.com/libp2p/js-libp2p-webtransport/commit/3772060df436f72976d9aaaa9d619ef5e7d93408)) + +## [1.0.11](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.10...v1.0.11) (2023-03-28) + + +### Bug Fixes + +* allow dialling ip6 webtransport addresses ([#60](https://github.com/libp2p/js-libp2p-webtransport/issues/60)) ([fe4612a](https://github.com/libp2p/js-libp2p-webtransport/commit/fe4612a37620203a04b70ec96acce7c890f2ec7d)) + +## [1.0.10](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.9...v1.0.10) (2023-03-24) + + +### Bug Fixes + +* window is not defined in worker contexts ([#59](https://github.com/libp2p/js-libp2p-webtransport/issues/59)) ([94c646b](https://github.com/libp2p/js-libp2p-webtransport/commit/94c646bdcdb9c1e5fa13d00a3ae03bc6c5727404)) + +## [1.0.9](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.8...v1.0.9) (2023-03-22) + + +### Dependencies + +* update @multiformats/multiaddr to 12.x.x ([#58](https://github.com/libp2p/js-libp2p-webtransport/issues/58)) ([1b3d005](https://github.com/libp2p/js-libp2p-webtransport/commit/1b3d005e4d82a2fec3e9ab32bb813cae9e073af2)) + + +### Documentation + +* **example:** add helper instructions for running example ([#56](https://github.com/libp2p/js-libp2p-webtransport/issues/56)) ([0f0d54b](https://github.com/libp2p/js-libp2p-webtransport/commit/0f0d54b56a60ea80a91f96b05b74f2d42f443a15)) + +## [1.0.8](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.7...v1.0.8) (2023-03-22) + + +### Trivial Changes + +* constrain go version examples run with ([#57](https://github.com/libp2p/js-libp2p-webtransport/issues/57)) ([aa177a8](https://github.com/libp2p/js-libp2p-webtransport/commit/aa177a8afcdb4fcccbd22cd97e8676fa00a44187)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([9435f0d](https://github.com/libp2p/js-libp2p-webtransport/commit/9435f0d1f9a93169e26789c6a3cb0706f06149cd)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([63f0c33](https://github.com/libp2p/js-libp2p-webtransport/commit/63f0c33bc2b876c11ec756e132bb679f32f46245)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([5e3a711](https://github.com/libp2p/js-libp2p-webtransport/commit/5e3a71197f1c8e89a9a5c05bdeee544bc3e460f1)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#54](https://github.com/libp2p/js-libp2p-webtransport/issues/54)) ([23bbd82](https://github.com/libp2p/js-libp2p-webtransport/commit/23bbd82bf2c3caa25d5964c98f7362736134862d)) + +## [1.0.7](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.6...v1.0.7) (2023-01-12) + + +### Dependencies + +* Update deps and add quic-v1 support ([#44](https://github.com/libp2p/js-libp2p-webtransport/issues/44)) ([d1613b1](https://github.com/libp2p/js-libp2p-webtransport/commit/d1613b10c1c8164fadcbe9a28175b7f0099d1645)), closes [#35](https://github.com/libp2p/js-libp2p-webtransport/issues/35) + + +### Documentation + +* update project config to publish api docs ([#45](https://github.com/libp2p/js-libp2p-webtransport/issues/45)) ([bdbf402](https://github.com/libp2p/js-libp2p-webtransport/commit/bdbf4025b5f80d6cc8af49596da09386538dc791)) + +## [1.0.6](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.5...v1.0.6) (2022-12-06) + + +### Bug Fixes + +* Make a fix release with [#32](https://github.com/libp2p/js-libp2p-webtransport/issues/32) ([#34](https://github.com/libp2p/js-libp2p-webtransport/issues/34)) ([66a38f6](https://github.com/libp2p/js-libp2p-webtransport/commit/66a38f6e452e72042ad10ef8521544d8b5afecff)) + +## [1.0.5](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.4...v1.0.5) (2022-11-16) + + +### Bug Fixes + +* Close stream after sink ([#23](https://github.com/libp2p/js-libp2p-webtransport/issues/23)) ([a95720c](https://github.com/libp2p/js-libp2p-webtransport/commit/a95720c367c8061ae45b4ae4bc4180e3ceea61cc)) + +## [1.0.4](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.3...v1.0.4) (2022-11-01) + + +### Documentation + +* Use textarea for multiaddr input in example ([#24](https://github.com/libp2p/js-libp2p-webtransport/issues/24)) ([14ce351](https://github.com/libp2p/js-libp2p-webtransport/commit/14ce351375dabb31df948005b20acff56acc483a)) + +## [1.0.3](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.2...v1.0.3) (2022-10-18) + + +### Documentation + +* add fetch-file-from-kubo example ([#12](https://github.com/libp2p/js-libp2p-webtransport/issues/12)) ([4a8f2f3](https://github.com/libp2p/js-libp2p-webtransport/commit/4a8f2f3eb4fdede1510aa2808d4c9a30d7ae86bf)) + +## [1.0.2](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.1...v1.0.2) (2022-10-17) + + +### Bug Fixes + +* update project, remove @libp2p/components and unused deps ([#20](https://github.com/libp2p/js-libp2p-webtransport/issues/20)) ([568638e](https://github.com/libp2p/js-libp2p-webtransport/commit/568638e9fddc57726547e9147647af468a28bf51)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.0...v1.0.1) (2022-10-12) + + +### Dependencies + +* bump multiformats from 9.9.0 to 10.0.0 ([c3f7d22](https://github.com/libp2p/js-libp2p-webtransport/commit/c3f7d220969de6ec8a632738f760ab11388ef3e7)) +* **dev:** bump @libp2p/interface-transport-compliance-tests ([62c8e6b](https://github.com/libp2p/js-libp2p-webtransport/commit/62c8e6b3c18959d7416767d307a5ebaac8c19ae8)) +* **dev:** bump protons from 5.1.0 to 6.0.0 ([03f7f33](https://github.com/libp2p/js-libp2p-webtransport/commit/03f7f33ba5561771746f1f1cfff7421da36c5889)) + +## 1.0.0 (2022-10-12) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([cc208ac](https://github.com/libp2p/js-libp2p-webtransport/commit/cc208acc1c3459e5ec5f230927bfe5dc6e175f39)) +* use rc version of libp2p ([bdc9dfb](https://github.com/libp2p/js-libp2p-webtransport/commit/bdc9dfb63f9853a38bc3b6999a1f986bff116dda)) + + +### Dependencies + +* 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)) diff --git a/packages/transport-webtransport/LICENSE b/packages/transport-webtransport/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-webtransport/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-webtransport/LICENSE-APACHE b/packages/transport-webtransport/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-webtransport/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-webtransport/LICENSE-MIT b/packages/transport-webtransport/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-webtransport/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/transport-webtransport/README.md b/packages/transport-webtransport/README.md new file mode 100644 index 0000000000..a7d41e4870 --- /dev/null +++ b/packages/transport-webtransport/README.md @@ -0,0 +1,93 @@ +# @libp2p/webtransport + +[![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 the WebTransport module that libp2p uses and that implements the interface-transport spec + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## Description + +`libp2p-webtransport` is the WebTransport transport implementation compatible with libp2p. + +## Usage + +```sh +> npm i @libp2p/webtransport +``` + +## Libp2p Usage Example + +```js +import { createLibp2pNode } from 'libp2p' +import { webTransport } from '@libp2p/webtransport' +import { noise } from 'libp2p-noise' + +const node = await createLibp2pNode({ + transports: [ + webTransport() + ], + connectionEncryption: [ + noise() + ] +}) +``` + +For more information see [libp2p/js-libp2p/doc/CONFIGURATION.md#customizing-transports](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#customizing-transports). + +## API + +### Transport + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) + +### Connection + +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore b/packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/README.md b/packages/transport-webtransport/examples/fetch-file-from-kubo/README.md new file mode 100644 index 0000000000..c02db34408 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/README.md @@ -0,0 +1,121 @@ +

      + + IPFS in JavaScript logo + +

      + +

      js-libp2p with WebTransport

      + +

      + js-libp2p using WebTransport! +
      +
      + +
      + Explore the docs + · + Report Bug + · + Request Feature/Example +

      + +## Table of Contents + +- [About The Project](#about-the-project) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation and Running example](#installation-and-running-example) +- [Usage](#usage) +- [References](#references) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Want to hack on IPFS?](#want-to-hack-on-ipfs) + +## About The Project + +- Read the [docs](https://github.com/ipfs/js-ipfs/tree/master/docs) +- Look into other [examples](https://github.com/ipfs-examples/js-ipfs-examples) to learn how to spawn an IPFS node in Node.js and in the Browser +- Consult the [Core API docs](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) to see what you can do with an IPFS node +- Visit https://dweb-primer.ipfs.io to learn about IPFS and the concepts that underpin it +- Head over to https://proto.school to take interactive tutorials that cover core IPFS APIs +- Check out https://docs.ipfs.io for tips, how-tos and more +- See https://blog.ipfs.io for news and more +- Need help? Please ask 'How do I?' questions on https://discuss.ipfs.io + +## Getting Started + +### Prerequisites + +Make sure you have installed all of the following prerequisites on your development machine: + +- Git - [Download & Install Git](https://git-scm.com/downloads). OSX and Linux machines typically have this already installed. +- Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. + +### Installation and Running example + +**Pre-requisite**: Because this example is in a subfolder of @libp2p/webtransport, if you are running the example inside https://github.com/libp2p/js-libp2p-webtransport, you must build at the root first. If you are running the code outside of https://github.com/libp2p/js-libp2p-webtransport, you must run `npm install --save @libp2p/webtransport` first. + +```console +> npm install +> npm start +``` + +Now open your browser at `http://localhost:8888` + +## Usage + +In this example, you will find a boilerplate you can use to guide yourself into bundling js-ipfs with [browserify](http://browserify.org/), so that you can use it in your own web app! + +You should see the following: + +![](./img/img1.png) +![](./img/img2.png) + +This example demonstrates the `Regular API`, top-level API for add, cat, get and ls Files on IPFS + +_For more examples, please refer to the [Documentation](#documentation)_ + +## References + +- Documentation: + - [IPFS CONFIG](https://github.com/ipfs/js-ipfs/blob/master/docs/CONFIG.md) + - [MISCELLANEOUS](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/MISCELLANEOUS.md) + - [FILES](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md) +- Tutorials: + - [MFS API](https://proto.school/mutable-file-system) + - [Regular File API](https://proto.school/regular-files-api) + +## Documentation + +- [Config](https://docs.ipfs.io/) +- [Core API](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) +- [Examples](https://github.com/ipfs-examples/js-ipfs-examples) +- [Development](https://github.com/ipfs/js-ipfs/blob/master/docs/DEVELOPMENT.md) +- [Tutorials](https://proto.school) + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the IPFS Project +2. Create your Feature Branch (`git checkout -b feature/amazing-feature`) +3. Commit your Changes (`git commit -a -m 'feat: add some amazing feature'`) +4. Push to the Branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## Want to hack on IPFS? + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) + +The IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out: + +Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md). + +- **Check out existing issues** The [issue list](https://github.com/ipfs/js-ipfs/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge +- **Look at the [IPFS Roadmap](https://github.com/ipfs/roadmap)** This are the high priority items being worked on right now +- **Perform code reviews** 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. +- **Join the [Weekly Core Implementations Call](https://github.com/ipfs/team-mgmt/issues/992)** it's where everyone discusses what's going on with IPFS and what's next diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img1.png b/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img1.png new file mode 100644 index 0000000000..6460e27446 Binary files /dev/null and b/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img1.png differ diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img2.png b/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img2.png new file mode 100644 index 0000000000..d04212d2d7 Binary files /dev/null and b/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img2.png differ diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/index.html b/packages/transport-webtransport/examples/fetch-file-from-kubo/index.html new file mode 100644 index 0000000000..99e6e29be8 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/index.html @@ -0,0 +1,37 @@ + + + + + + + js-libp2p WebTransport + + + +
      +
      +

      Connect to (multiaddr with p2p):

      + + +
      + + +
      + + +
      + +
      + + + + \ No newline at end of file diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/package.json b/packages/transport-webtransport/examples/fetch-file-from-kubo/package.json new file mode 100644 index 0000000000..143eeb2ea6 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/package.json @@ -0,0 +1,26 @@ +{ + "name": "fetch-file-from-kubo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "npm run build && test-browser-example tests" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.1", + "@libp2p/webtransport": "../..", + "@multiformats/multiaddr": "^12.1.2", + "blockstore-core": "^4.1.0", + "ipfs-bitswap": "^18.0.1", + "libp2p": "^0.45.9", + "multiformats": "^11.0.2" + }, + "devDependencies": { + "test-ipfs-example": "^1.0.0", + "typescript": "^4.6.4", + "vite": "^3.1.0" + } +} diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts new file mode 100644 index 0000000000..3d97d38243 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts @@ -0,0 +1,27 @@ +import { webTransport } from '@libp2p/webtransport' +import { noise } from '@chainsafe/libp2p-noise' +import { createLibp2p, Libp2p } from 'libp2p' +import { createBitswap } from 'ipfs-bitswap' +import { MemoryBlockstore } from 'blockstore-core/memory' + +type Bitswap = ReturnType + +export async function setup (): Promise<{ libp2p: Libp2p, bitswap: Bitswap }> { + const store = new MemoryBlockstore() + + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + // this is only necessary when dialing local addresses + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + await node.start() + + const bitswap = createBitswap(node, store) + await bitswap.start() + + return { libp2p: node, bitswap } +} diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts new file mode 100644 index 0000000000..e2279bdf4c --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts @@ -0,0 +1,64 @@ +import './style.css' +import { multiaddr } from '@multiformats/multiaddr' +import { setup as libp2pSetup } from './libp2p' +import { CID } from 'multiformats/cid' + +localStorage.debug = '*' + +declare global { + interface Window { + fetchBtn: HTMLButtonElement + connectBtn: HTMLButtonElement + peerInput: HTMLInputElement + cidInput: HTMLInputElement + statusEl: HTMLParagraphElement + downloadEl: HTMLAnchorElement + downloadCidWrapperEl: HTMLDivElement + connlistWrapperEl: HTMLDivElement + connlistEl: HTMLUListElement + } +} + +(async function () { + const { libp2p, bitswap } = await libp2pSetup() + window.connectBtn.onclick = async () => { + const ma = multiaddr(window.peerInput.value) + await libp2p.dial(ma) + } + + libp2p.addEventListener('peer:connect', (_connection) => { + updateConnList() + }) + libp2p.addEventListener('peer:disconnect', (_connection) => { + updateConnList() + }) + + function updateConnList () { + const addrs = libp2p.getConnections().map(c => c.remoteAddr.toString()) + if (addrs.length > 0) { + window.downloadCidWrapperEl.hidden = false + window.connlistWrapperEl.hidden = false + window.connlistEl.innerHTML = '' + addrs.forEach(a => { + const li = document.createElement('li') + li.innerText = a + window.connlistEl.appendChild(li) + }) + } else { + window.downloadCidWrapperEl.hidden = true + window.connlistWrapperEl.hidden = true + window.connlistEl.innerHTML = '' + } + } + + window.fetchBtn.onclick = async () => { + const c = CID.parse(window.cidInput.value) + window.statusEl.hidden = false + const val = await bitswap.want(c) + window.statusEl.hidden = true + + window.downloadEl.href = window.URL.createObjectURL(new Blob([val], { type: 'bytes' })) + window.downloadEl.hidden = false + } +// eslint-disable-next-line no-console +})().catch(err => console.error(err)) diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css new file mode 100644 index 0000000000..072f654118 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css @@ -0,0 +1,109 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #3178c6aa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + +#connlistWrapperEl ul { + max-width: 400px; + overflow-x: auto; +} \ No newline at end of file diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js b/packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js new file mode 100644 index 0000000000..edebee8011 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js @@ -0,0 +1,76 @@ +/* eslint-disable no-console */ +import { setup, expect } from 'test-ipfs-example/browser' +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' + +// Setup +const test = setup() + +async function spinUpGoLibp2p() { + if (!existsSync('../../go-libp2p-webtransport-server/main')) { + await new Promise((resolve, reject) => { + exec('go build -o main main.go', + { cwd: '../../go-libp2p-webtransport-server' }, + (error, stdout, stderr) => { + if (error) { + reject(error) + console.error(`exec error: ${error}`) + return + } + resolve() + }) + }) + } + + const server = spawn('./main', [], { cwd: '../../go-libp2p-webtransport-server', killSignal: 'SIGINT' }) + server.stderr.on('data', (data) => { + console.log(`stderr: ${data}`, typeof data) + }) + const serverAddr = await (new Promise(resolve => { + server.stdout.on('data', (data) => { + console.log(`stdout: ${data}`, typeof data) + if (data.includes('addr=')) { + // Parse the addr out + resolve((String(data)).match(/addr=([^\s]*)/)[1]) + } + }) + })) + return { server, serverAddr } +} + +test.describe('bundle ipfs with parceljs:', () => { + // DOM + const connectBtn = '#connectBtn' + const connectAddr = '#peerInput' + const connList = '#connlistEl' + + let server + let serverAddr + + // eslint-disable-next-line no-empty-pattern + test.beforeAll(async ({ }, testInfo) => { + testInfo.setTimeout(5 * 60_000) + const s = await spinUpGoLibp2p() + server = s.server + serverAddr = s.serverAddr + console.log('Server addr:', serverAddr) + }, {}) + + test.afterAll(() => { + server.kill('SIGINT') + }) + + test.beforeEach(async ({ servers, page }) => { + await page.goto(servers[0].url) + }) + + test('should connect to a go-libp2p node over webtransport', async ({ page }) => { + await page.fill(connectAddr, serverAddr) + await page.click(connectBtn) + + await page.waitForSelector('#connlistEl:has(li)') + + const connections = await page.textContent(connList) + expect(connections).toContain(serverAddr) + }) +}) diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json b/packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json new file mode 100644 index 0000000000..fbd022532d --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js b/packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js new file mode 100644 index 0000000000..bcbaff69d7 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js @@ -0,0 +1,8 @@ +export default { + build: { + target: 'es2020' + }, + optimizeDeps: { + esbuildOptions: { target: 'es2020', supported: { bigint: true } } + } +} diff --git a/packages/transport-webtransport/go-libp2p-webtransport-server/go.mod b/packages/transport-webtransport/go-libp2p-webtransport-server/go.mod new file mode 100644 index 0000000000..e74c5bfb4a --- /dev/null +++ b/packages/transport-webtransport/go-libp2p-webtransport-server/go.mod @@ -0,0 +1,95 @@ +module github.com/libp2p/js-libp2p-webtransport/go-libp2p-webtransport-server/m/v2 + +go 1.19 + +require ( + github.com/libp2p/go-libp2p v0.27.1 + github.com/multiformats/go-multiaddr v0.9.0 +) + +require ( + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.2 // indirect + github.com/flynn/noise v1.0.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/huin/goupnp v1.1.0 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/compress v1.16.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-nat v0.1.0 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect + github.com/libp2p/go-reuseport v0.2.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.53 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.8.1 // indirect + github.com/multiformats/go-multihash v0.2.1 // indirect + github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/onsi/ginkgo/v2 v2.9.2 // indirect + github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.3.2 // indirect + github.com/quic-go/qtls-go1-20 v0.2.2 // indirect + github.com/quic-go/quic-go v0.33.0 // indirect + github.com/quic-go/webtransport-go v0.5.2 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/dig v1.16.1 // indirect + go.uber.org/fx v1.19.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + lukechampine.com/blake3 v1.1.7 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/packages/transport-webtransport/go-libp2p-webtransport-server/go.sum b/packages/transport-webtransport/go-libp2p-webtransport-server/go.sum new file mode 100644 index 0000000000..8b98f347e4 --- /dev/null +++ b/packages/transport-webtransport/go-libp2p-webtransport-server/go.sum @@ -0,0 +1,501 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= +github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b h1:Qcx5LM0fSiks9uCyFZwDBUasd3lxd1RM0GYpL+Li5o4= +github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= +github.com/huin/goupnp v1.1.0 h1:gEe0Dp/lZmPZiDFzJJaOfUpOvv2MKUkoBX8lDrn9vKU= +github.com/huin/goupnp v1.1.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU= +github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= +github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-libp2p v0.27.1 h1:k1u6RHsX3hqKnslDjsSgLNURxJ3O1atIZCY4gpMbbus= +github.com/libp2p/go-libp2p v0.27.1/go.mod h1:FAvvfQa/YOShUYdiSS03IR9OXzkcJXwcNA2FUCh9ImE= +github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= +github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= +github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= +github.com/libp2p/go-netroute v0.1.2/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= +github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= +github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= +github.com/libp2p/go-sockaddr v0.0.2/go.mod h1:syPvOmNs24S3dFVGJA1/mrqdeijPxLV2Le3BRLKd68k= +github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ= +github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= +github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= +github.com/multiformats/go-multiaddr v0.9.0 h1:3h4V1LHIk5w4hJHekMKWALPXErDfz/sggzwC/NcqbDQ= +github.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc46ElrvzhYnoJOmTT0= +github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= +github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= +github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= +github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= +github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= +github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= +github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= +github.com/quic-go/webtransport-go v0.5.2 h1:GA6Bl6oZY+g/flt00Pnu0XtivSD8vukOu3lYhJjnGEk= +github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= +go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= +go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= +go.uber.org/fx v1.19.2/go.mod h1:43G1VcqSzbIv77y00p1DRAsyZS8WdzuYdhZXmEUkMyQ= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= +lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/packages/transport-webtransport/go-libp2p-webtransport-server/main.go b/packages/transport-webtransport/go-libp2p-webtransport-server/main.go new file mode 100644 index 0000000000..6a388e2ab0 --- /dev/null +++ b/packages/transport-webtransport/go-libp2p-webtransport-server/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "os" + "os/signal" + "time" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/network" + webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport" + "github.com/multiformats/go-multiaddr" +) + +func main() { + h, err := libp2p.New(libp2p.Transport(webtransport.New)) + if err != nil { + panic(err) + } + + err = h.Network().Listen(multiaddr.StringCast("/ip4/127.0.0.1/udp/0/quic-v1/webtransport")) + if err != nil { + panic(err) + } + + err = h.Network().Listen(multiaddr.StringCast("/ip6/::1/udp/0/quic-v1/webtransport")) + if err != nil { + panic(err) + } + + h.SetStreamHandler("echo", func(s network.Stream) { + io.Copy(s, s) + s.Close() + }) + + for _, a := range h.Addrs() { + withP2p := a.Encapsulate(multiaddr.StringCast("/p2p/" + h.ID().String())) + fmt.Printf("addr=%s\n", withP2p.String()) + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + select { + case <-c: + case <-time.After(time.Minute): + } +} diff --git a/packages/transport-webtransport/package.json b/packages/transport-webtransport/package.json new file mode 100644 index 0000000000..9c8c6300ac --- /dev/null +++ b/packages/transport-webtransport/package.json @@ -0,0 +1,90 @@ +{ + "name": "@libp2p/webtransport", + "version": "2.0.2", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "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": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t browser -t webworker", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.1", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-stream-muxer": "^4.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "it-stream-types": "^2.0.1", + "multiformats": "^11.0.2", + "uint8arraylist": "^2.4.3" + }, + "devDependencies": { + "aegir": "^39.0.10", + "libp2p": "^0.45.0", + "p-defer": "^4.0.0" + }, + "browser": { + "./dist/src/listener.js": "./dist/src/listener.browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-webtransport/src/index.ts b/packages/transport-webtransport/src/index.ts new file mode 100644 index 0000000000..d10b5b559d --- /dev/null +++ b/packages/transport-webtransport/src/index.ts @@ -0,0 +1,503 @@ +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 { peerIdFromString } from '@libp2p/peer-id' +import { type Multiaddr, protocols } from '@multiformats/multiaddr' +import { bases, digest } from 'multiformats/basics' +import { Uint8ArrayList } from 'uint8arraylist' +import type { Connection, Direction, MultiaddrConnection, Stream } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { StreamMuxerFactory, StreamMuxerInit, StreamMuxer } from '@libp2p/interface-stream-muxer' +import type { Duplex, Source } from 'it-stream-types' +import type { MultihashDigest } from 'multiformats/hashes/interface' + +declare global { + var WebTransport: any +} + +const log = logger('libp2p:webtransport') + +// @ts-expect-error - Not easy to combine these types. +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b)) + +function decodeCerthashStr (s: string): MultihashDigest { + return digest.decode(multibaseDecoder.decode(s)) +} + +// Duplex that does nothing. Needed to fulfill the interface +function inertDuplex (): Duplex { + return { + source: { + [Symbol.asyncIterator] () { + return { + async next () { + // This will never resolve + return new Promise(() => { }) + } + } + } + }, + sink: async (source: Source) => { + // This will never resolve + return new Promise(() => { }) + } + } +} + +async function webtransportBiDiStreamToStream (bidiStream: any, 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 + + function cleanupStreamFromActiveStreams (): void { + const index = activeStreams.findIndex(s => s === stream) + if (index !== -1) { + activeStreams.splice(index, 1) + stream.stat.timeline.close = Date.now() + onStreamEnd?.(stream) + } + } + + let writerClosed = false + let readerClosed = false; + (async function () { + const err: Error | undefined = await writer.closed.catch((err: Error) => err) + if (err != null) { + const msg = err.message + if (!(msg.includes('aborted by the remote server') || msg.includes('STOP_SENDING'))) { + log.error(`WebTransport writer closed unexpectedly: streamId=${streamId} err=${err.message}`) + } + } + writerClosed = true + if (writerClosed && readerClosed) { + cleanupStreamFromActiveStreams() + } + })().catch(() => { + log.error('WebTransport failed to cleanup closed stream') + }); + + (async function () { + const err: Error | undefined = await reader.closed.catch((err: Error) => err) + if (err != null) { + log.error(`WebTransport reader closed unexpectedly: streamId=${streamId} err=${err.message}`) + } + readerClosed = true + if (writerClosed && readerClosed) { + cleanupStreamFromActiveStreams() + } + })().catch(() => { + log.error('WebTransport failed to cleanup closed stream') + }) + + let sinkSunk = false + const stream: Stream = { + id: streamId, + abort (_err: Error) { + if (!writerClosed) { + writer.abort() + writerClosed = true + } + stream.closeRead() + readerClosed = true + cleanupStreamFromActiveStreams() + }, + close () { + stream.closeRead() + stream.closeWrite() + cleanupStreamFromActiveStreams() + }, + + closeRead () { + if (!readerClosed) { + reader.cancel().catch((err: any) => { + if (err.toString().includes('RESET_STREAM') === true) { + writerClosed = true + } + }) + readerClosed = true + } + if (writerClosed) { + cleanupStreamFromActiveStreams() + } + }, + closeWrite () { + if (!writerClosed) { + writerClosed = true + writer.close().catch((err: any) => { + if (err.toString().includes('RESET_STREAM') === true) { + readerClosed = true + } + }) + } + if (readerClosed) { + cleanupStreamFromActiveStreams() + } + }, + reset () { + stream.close() + }, + stat: { + direction, + timeline: { open: Date.now() } + }, + metadata: {}, + source: (async function * () { + while (true) { + const val = await reader.read() + if (val.done === true) { + readerClosed = true + if (writerClosed) { + cleanupStreamFromActiveStreams() + } + return + } + + yield new Uint8ArrayList(val.value) + } + })(), + sink: async function (source: Source) { + if (sinkSunk) { + throw new Error('sink already called on stream') + } + sinkSunk = true + try { + for await (const chunks of source) { + if (chunks instanceof Uint8Array) { + await writer.write(chunks) + } else { + for (const buf of chunks) { + await writer.write(buf) + } + } + } + } finally { + stream.closeWrite() + } + } + } + + return stream +} + +function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDigest[], remotePeer?: PeerId } { + const parts = ma.stringTuples() + + // 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: [] }) + + return { url, certhashes, remotePeer } +} + +// Determines if `maybeSubset` is a subset of `set`. This means that all byte arrays in `maybeSubset` are present in `set`. +export function isSubset (set: Uint8Array[], maybeSubset: Uint8Array[]): boolean { + const intersection = maybeSubset.filter(byteArray => { + return Boolean(set.find((otherByteArray: Uint8Array) => { + if (byteArray.length !== otherByteArray.length) { + return false + } + + for (let index = 0; index < byteArray.length; index++) { + if (otherByteArray[index] !== byteArray[index]) { + return false + } + } + return true + })) + }) + return (intersection.length === maybeSubset.length) +} + +export interface WebTransportInit { + maxInboundStreams?: number +} + +export interface WebTransportComponents { + peerId: PeerId +} + +class WebTransportTransport implements Transport { + private readonly components: WebTransportComponents + private readonly config: Required + + constructor (components: WebTransportComponents, init: WebTransportInit = {}) { + this.components = components + this.config = { + maxInboundStreams: init.maxInboundStreams ?? 1000 + } + } + + readonly [Symbol.toStringTag] = '@libp2p/webtransport' + + readonly [symbol] = true + + async dial (ma: Multiaddr, options: DialOptions): Promise { + log('dialing %s', ma) + const localPeer = this.components.peerId + if (localPeer === undefined) { + throw new Error('Need a local peerid') + } + + options = options ?? {} + + const { url, certhashes, remotePeer } = parseMultiaddr(ma) + + if (certhashes.length === 0) { + throw new Error('Expected multiaddr to contain certhashes') + } + + const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, { + serverCertificateHashes: certhashes.map(certhash => ({ + algorithm: 'sha-256', + value: certhash.digest + })) + }) + wt.closed.catch((error: Error) => { + log.error('WebTransport transport closed due to:', error) + }) + await wt.ready + + if (remotePeer == null) { + throw new Error('Need a target peerid') + } + + if (!await this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes)) { + throw new Error('Failed to authenticate webtransport') + } + + const maConn: MultiaddrConnection = { + close: async (err?: Error) => { + if (err != null) { + log('Closing webtransport with err:', err) + } + wt.close() + }, + remoteAddr: ma, + timeline: { + open: Date.now() + }, + // This connection is never used directly since webtransport supports native streams. + ...inertDuplex() + } + + wt.closed.catch((err: Error) => { + log.error('WebTransport connection closed:', err) + // This is how we specify the connection is closed and shouldn't be used. + maConn.timeline.close = Date.now() + }) + + try { + options?.signal?.throwIfAborted() + } catch (e) { + wt.close() + throw e + } + + return options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt), skipProtection: true }) + } + + async authenticateWebTransport (wt: InstanceType, localPeer: PeerId, remotePeer: PeerId, certhashes: Array>): Promise { + const stream = await wt.createBidirectionalStream() + const writer = stream.writable.getWriter() + const reader = stream.readable.getReader() + await writer.ready + + const duplex = { + source: (async function * () { + while (true) { + const val = await reader.read() + + if (val.value != null) { + yield val.value + } + + if (val.done === true) { + break + } + } + })(), + sink: async function (source: Source) { + for await (const chunk of source) { + await writer.write(chunk) + } + } + } + + const n = noise()() + + const { remoteExtensions } = await n.secureOutbound(localPeer, duplex, remotePeer) + + // We're done with this authentication stream + writer.close().catch((err: Error) => { + log.error(`Failed to close authentication stream writer: ${err.message}`) + }) + + reader.cancel().catch((err: Error) => { + log.error(`Failed to close authentication stream reader: ${err.message}`) + }) + + // Verify the certhashes we used when dialing are a subset of the certhashes relayed by the remote peer + if (!isSubset(remoteExtensions?.webtransportCerthashes ?? [], certhashes.map(ch => ch.bytes))) { + throw new Error("Our certhashes are not a subset of the remote's reported certhashes") + } + + return true + } + + webtransportMuxer (wt: InstanceType): StreamMuxerFactory { + let streamIDCounter = 0 + const config = this.config + return { + protocol: 'webtransport', + createStreamMuxer: (init?: StreamMuxerInit): StreamMuxer => { + // !TODO handle abort signal when WebTransport supports this. + + if (typeof init === 'function') { + // The api docs say that init may be a function + init = { onIncomingStream: init } + } + + const activeStreams: Stream[] = []; + + (async function () { + //! TODO unclear how to add backpressure here? + + const reader = wt.incomingBidirectionalStreams.getReader() + while (true) { + const { done, value: wtStream } = await reader.read() + + if (done === true) { + break + } + + if (activeStreams.length >= config.maxInboundStreams) { + // We've reached our limit, close this stream. + wtStream.writable.close().catch((err: Error) => { + log.error(`Failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`) + }) + wtStream.readable.cancel().catch((err: Error) => { + log.error(`Failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`) + }) + } else { + const stream = await webtransportBiDiStreamToStream(wtStream, String(streamIDCounter++), 'inbound', activeStreams, init?.onStreamEnd) + activeStreams.push(stream) + init?.onIncomingStream?.(stream) + } + } + })().catch(() => { + log.error('WebTransport failed to receive incoming stream') + }) + + const muxer: StreamMuxer = { + protocol: 'webtransport', + streams: activeStreams, + newStream: async (name?: string): Promise => { + const wtStream = await wt.createBidirectionalStream() + + const stream = await webtransportBiDiStreamToStream(wtStream, String(streamIDCounter++), init?.direction ?? 'outbound', activeStreams, init?.onStreamEnd) + activeStreams.push(stream) + + return stream + }, + + /** + * Close or abort all tracked streams and stop the muxer + */ + close: (err?: Error) => { + if (err != null) { + log('Closing webtransport muxer with err:', err) + } + wt.close() + }, + // This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex. + ...inertDuplex() + } + + try { + init?.signal?.throwIfAborted() + } catch (e) { + wt.close() + throw e + } + + return muxer + } + } + } + + createListener (options: CreateListenerOptions): Listener { + throw new Error('Webtransport servers are not supported in Node or the browser') + } + + /** + * Takes a list of `Multiaddr`s and returns only valid webtransport addresses. + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter(ma => ma.protoNames().includes('webtransport')) + } +} + +export function webTransport (init: WebTransportInit = {}): (components: WebTransportComponents) => Transport { + return (components: WebTransportComponents) => new WebTransportTransport(components, init) +} diff --git a/packages/transport-webtransport/test/browser.ts b/packages/transport-webtransport/test/browser.ts new file mode 100644 index 0000000000..153743e62b --- /dev/null +++ b/packages/transport-webtransport/test/browser.ts @@ -0,0 +1,189 @@ +/* eslint-disable no-console */ +/* eslint-env mocha */ + +import { noise } from '@chainsafe/libp2p-noise' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import { webTransport, isSubset } from '../src/index' + +declare global { + interface Window { + WebTransport: any + } +} + +describe('libp2p-webtransport', () => { + it('webtransport connects to go-libp2p', async () => { + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr + const ma = multiaddr(maStr) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + await node.start() + + // Ping many times + for (let index = 0; index < 100; index++) { + const now = Date.now() + + // Note we're re-implementing the ping protocol here because as of this + // writing, go-libp2p will reset the stream instead of close it. The next + // version of go-libp2p v0.24.0 will have this fix. When that's released + // we can use the builtin ping system + const stream = await node.dialProtocol(ma, '/ipfs/ping/1.0.0') + + const data = new Uint8Array(32) + globalThis.crypto.getRandomValues(data) + + const pong = new Promise((resolve, reject) => { + (async () => { + for await (const chunk of stream.source) { + const v = chunk.subarray() + const byteMatches: boolean = v.every((byte: number, i: number) => byte === data[i]) + if (byteMatches) { + resolve() + } else { + reject(new Error('Wrong pong')) + } + } + })().catch(reject) + }) + + let res = -1 + await stream.sink((async function * () { + yield data + // Wait for the pong before we close the write side + await pong + res = Date.now() - now + })()) + + stream.close() + + expect(res).to.be.greaterThan(-1) + } + + await node.stop() + const conns = node.getConnections() + expect(conns.length).to.equal(0) + }) + + it('fails to connect without certhashes', async () => { + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr + const maStrNoCerthash: string = maStr.split('/certhash')[0] + const maStrP2p = maStr.split('/p2p/')[1] + const ma = multiaddr(maStrNoCerthash + '/p2p/' + maStrP2p) + + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + await node.start() + + const err = await expect(node.dial(ma)).to.eventually.be.rejected() + expect(err.toString()).to.contain('Expected multiaddr to contain certhashes') + + await node.stop() + }) + + it('connects to ipv6 addresses', async () => { + if (process.env.serverAddr6 == null) { + throw new Error('serverAddr6 not found') + } + + const ma = multiaddr(process.env.serverAddr6) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + await node.start() + + // the address is unreachable but we can parse it correctly + const stream = await node.dialProtocol(ma, '/ipfs/ping/1.0.0') + stream.close() + + await node.stop() + }) + + it('Closes writes of streams after they have sunk a source', async () => { + // This is the behavior of stream muxers: (see mplex, yamux and compliance tests: https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interface-stream-muxer-compliance-tests/src/close-test.ts) + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr + const ma = multiaddr(maStr) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + async function * gen (): AsyncGenerator { + yield new Uint8Array([0]) + yield new Uint8Array([1, 2, 3, 4]) + yield new Uint8Array([5, 6, 7]) + yield new Uint8Array([8, 9, 10, 11]) + yield new Uint8Array([12, 13, 14, 15]) + } + + await node.start() + const stream = await node.dialProtocol(ma, 'echo') + + await stream.sink(gen()) + + let expectedNextNumber = 0 + for await (const chunk of stream.source) { + for (const byte of chunk.subarray()) { + expect(byte).to.equal(expectedNextNumber++) + } + } + expect(expectedNextNumber).to.equal(16) + + // Close read, we've should have closed the write side during sink + stream.closeRead() + + expect(stream.stat.timeline.close).to.be.greaterThan(0) + + await node.stop() + }) +}) + +describe('test helpers', () => { + it('correctly checks subsets', () => { + const testCases = [ + { a: [[1, 2, 3]], b: [[4, 5, 6]], isSubset: false }, + { a: [[1, 2, 3], [4, 5, 6]], b: [[1, 2, 3]], isSubset: true }, + { a: [[1, 2, 3], [4, 5, 6]], b: [], isSubset: true }, + { a: [], b: [[1, 2, 3]], isSubset: false }, + { a: [], b: [], isSubset: true }, + { a: [[1, 2, 3]], b: [[1, 2, 3], [4, 5, 6]], isSubset: false }, + { a: [[1, 2, 3]], b: [[1, 2]], isSubset: false } + ] + + for (const tc of testCases) { + expect(isSubset(tc.a.map(b => new Uint8Array(b)), tc.b.map(b => new Uint8Array(b)))).to.equal(tc.isSubset) + } + }) +}) diff --git a/packages/transport-webtransport/tsconfig.json b/packages/transport-webtransport/tsconfig.json new file mode 100644 index 0000000000..4f3f92ea0b --- /dev/null +++ b/packages/transport-webtransport/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-stream-muxer" + }, + { + "path": "../interface-transport" + }, + { + "path": "../libp2p" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/utils/API.md b/packages/utils/API.md new file mode 100644 index 0000000000..3dbf66d3e6 --- /dev/null +++ b/packages/utils/API.md @@ -0,0 +1,209 @@ +# API + +* [addressSort.publicAddressesFirst(addresses)](#addresssortpublicaddressesfirstaddresses) + * [Parameters](#parameters) + * [Returns](#returns) + * [Example](#example) +* [arrayEquals(a, b)](#arrayequalsa-b) + * [Parameters](#parameters) + * [Returns](#returns) + * [Example](#example) +* [multiaddr .isLoopback(ma)](#multiaddr-isloopbackma) + * [Parameters](#parameters-1) + * [Returns](#returns-1) + * [Example](#example-1) +* [multiaddr .isPrivate(ma)](#multiaddr-isprivatema) + * [Parameters](#parameters-2) + * [Returns](#returns-2) + * [Example](#example-2) +* [ipPortToMultiaddr(ip, port)](#ipporttomultiaddrip-port) + * [Parameters](#parameters-3) + * [Returns](#returns-3) + * [Example](#example-3) +* [streamToMaConnection(streamProperties, options)](#streamtomaconnectionstreamproperties-options) + * [Parameters](#parameters-4) + * [Returns](#returns-4) + * [Example](#example-4) + +## addressSort.publicAddressesFirst(addresses) + +Sort given addresses by putting public addresses first. In case of equality, a certified address will come first. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| addresses | `Array
      ` | Array of AddressBook addresses | + +### Returns + +| Type | Description | +|------|-------------| +| `Array
      ` | returns array of sorted addresses | + +### Example + +```js +const multiaddr = require('multiaddr') +const { publicAddressesFirst } = require('libp2p-utils/src/address-sort') + +const addresses = [ + { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + } +] + +const sortedAddresses = publicAddressesFirst(addresses) +``` + +## arrayEquals(a, b) + +Verify if two arrays of non primitive types with the "equals" function are equal. +Compatible with multiaddr, peer-id and others. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| a | `Array<*>` | First array to verify | +| b | `Array<*>` | Second array to verify | + +### Returns + +| Type | Description | +|------|-------------| +| `boolean` | returns true if arrays are equal, false otherwise | + +### Example + +```js +const PeerId = require('peer-id') +const arrayEquals = require('libp2p-utils/src/array-equals') + +const peerId1 = await PeerId.create() +const peerId2 = await PeerId.create() + +const equals = arrayEquals([peerId1], [peerId2]) +``` + +## multiaddr `.isLoopback(ma)` + +Check if a given multiaddr is a loopback address. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| ma | `Multiaddr` | multiaddr to verify | + +### Returns + +| Type | Description | +|------|-------------| +| `boolean` | returns true if multiaddr is a loopback address, false otherwise | + +### Example + +```js +const multiaddr = require('multiaddr') +const isLoopback = require('libp2p-utils/src/multiaddr/is-loopback') + +const ma = multiaddr('/ip4/127.0.0.1/tcp/1000') +isMultiaddrLoopbackAddrs = isLoopback(ma) +``` + +## multiaddr `.isPrivate(ma)` + +Check if a given multiaddr has a private address. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| ma | `Multiaddr` | multiaddr to verify | + +### Returns + +| Type | Description | +|------|-------------| +| `boolean` | returns true if multiaddr is a private address, false otherwise | + +### Example + +```js +const multiaddr = require('multiaddr') +const isPrivate = require('libp2p-utils/src/multiaddr/is-private') + +const ma = multiaddr('/ip4/10.0.0.1/tcp/1000') +isMultiaddrPrivateAddrs = isPrivate(ma) +``` + +## ipPortToMultiaddr(ip, port) + +Transform an IP, Port pair into a multiaddr with tcp transport. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| ip | `string` | ip for multiaddr | +| port | `number|string` | port for multiaddr | + +### Returns + +| Type | Description | +|------|-------------| +| `Multiaddr` | returns created multiaddr | + +### Example + +```js +const ipPortPairToMultiaddr = require('libp2p-utils/src/multiaddr/ip-port-to-multiaddr') +const ip = '127.0.0.1' +const port = '9090' + +const ma = ipPortPairToMultiaddr(ma) +``` + +## streamToMaConnection(streamProperties, options) + +Convert a duplex stream into a [MultiaddrConnection](https://github.com/libp2p/interface-transport#multiaddrconnection). + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| streamProperties | `object` | duplex stream properties | +| streamProperties.stream | [`DuplexStream`](https://github.com/libp2p/js-libp2p/blob/master/doc/STREAMING_ITERABLES.md#duplex) | duplex stream | +| streamProperties.remoteAddr | `Multiaddr` | stream remote address | +| streamProperties.localAddr | `Multiaddr` | stream local address | +| [options] | `object` | options | +| [options.signal] | `AbortSignal` | abort signal | + +### Returns + +| Type | Description | +|------|-------------| +| `Connection` | returns a multiaddr [Connection](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/connection) | + +### Example + +```js +const streamToMaConnection = require('libp2p-utils/src/stream-to-ma-conn') + +const stream = { + sink: async source => {/* ... */}, + source: { [Symbol.asyncIterator] () {/* ... */} } +} + +const conn = streamToMaConnection({ + stream, + remoteAddr: /* ... */ + localAddr; /* ... */ +}) +``` diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md new file mode 100644 index 0000000000..8ed268837a --- /dev/null +++ b/packages/utils/CHANGELOG.md @@ -0,0 +1,324 @@ +## [3.0.12](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.11...v3.0.12) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([2c91adc](https://github.com/libp2p/js-libp2p-utils/commit/2c91adc7e17fadd9f96c0fc222fb5557df037459)) +* Update .github/workflows/stale.yml [skip ci] ([e5fbee9](https://github.com/libp2p/js-libp2p-utils/commit/e5fbee99f549ad708dd375516356c5b20915cf87)) + + +### Dependencies + +* **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)) + +## [3.0.11](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.10...v3.0.11) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#91](https://github.com/libp2p/js-libp2p-utils/issues/91)) ([c7569d7](https://github.com/libp2p/js-libp2p-utils/commit/c7569d77a56d5fc3a5323c89ba93230206c35d2b)) + +## [3.0.10](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.9...v3.0.10) (2023-04-18) + + +### Dependencies + +* bump it-stream-types from 1.0.5 to 2.0.1 ([#89](https://github.com/libp2p/js-libp2p-utils/issues/89)) ([0de4a85](https://github.com/libp2p/js-libp2p-utils/commit/0de4a85bd6caa3dfec673ceb3be9130d4051e407)) + +## [3.0.9](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.8...v3.0.9) (2023-04-18) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#85](https://github.com/libp2p/js-libp2p-utils/issues/85)) ([b029517](https://github.com/libp2p/js-libp2p-utils/commit/b0295176c2c6553209ebb26149497a5f9a73fc9e)) +* **dev:** bump it-map from 2.0.1 to 3.0.2 ([#88](https://github.com/libp2p/js-libp2p-utils/issues/88)) ([6e24d5a](https://github.com/libp2p/js-libp2p-utils/commit/6e24d5a91b4df40c7a395d3d3c9379120de0754d)) + +## [3.0.8](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.7...v3.0.8) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#90](https://github.com/libp2p/js-libp2p-utils/issues/90)) ([d140507](https://github.com/libp2p/js-libp2p-utils/commit/d140507f1d4263886c515f4877425a01f28b88e7)) + +## [3.0.7](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.6...v3.0.7) (2023-03-31) + + +### Dependencies + +* **dev:** bump it-pipe from 2.0.5 to 3.0.0 ([#87](https://github.com/libp2p/js-libp2p-utils/issues/87)) ([fc28634](https://github.com/libp2p/js-libp2p-utils/commit/fc286345ff55e23b7619da2bdcedfd848d7c1f85)) + +## [3.0.6](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.5...v3.0.6) (2023-03-27) + + +### Bug Fixes + +* handle non ip4/ip6/dns addresses in isPrivate ([#84](https://github.com/libp2p/js-libp2p-utils/issues/84)) ([af2c222](https://github.com/libp2p/js-libp2p-utils/commit/af2c2221ad175a06f758a45fc71fbb2f870eece4)) + +## [3.0.5](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.4...v3.0.5) (2023-03-17) + + +### Trivial Changes + +* replace err-code with CodeError ([#70](https://github.com/libp2p/js-libp2p-utils/issues/70)) ([beb252d](https://github.com/libp2p/js-libp2p-utils/commit/beb252d79f69d0f49d1fa4fd664a49e33ff80cd3)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([acad1fe](https://github.com/libp2p/js-libp2p-utils/commit/acad1fe38a1cfef19f63de7283e721caec059d34)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1b96837](https://github.com/libp2p/js-libp2p-utils/commit/1b96837cac6c9625ed243d0f62595582a57f7f04)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([10d6e7a](https://github.com/libp2p/js-libp2p-utils/commit/10d6e7a7731b746f199ffb2f186e28185cb512f5)) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#83](https://github.com/libp2p/js-libp2p-utils/issues/83)) ([3eeeeba](https://github.com/libp2p/js-libp2p-utils/commit/3eeeeba52b764b96463a1b6bcfcff394492eab2e)) +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#80](https://github.com/libp2p/js-libp2p-utils/issues/80)) ([2c262ba](https://github.com/libp2p/js-libp2p-utils/commit/2c262ba37d3668bc4f957914c40c5167cd8faf4f)) + +## [3.0.4](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.3...v3.0.4) (2022-12-16) + + +### Documentation + +* publish api docs ([#69](https://github.com/libp2p/js-libp2p-utils/issues/69)) ([044fd72](https://github.com/libp2p/js-libp2p-utils/commit/044fd7232eb0be2d8cd71fab6130c4f30190e22b)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.2...v3.0.3) (2022-12-07) + + +### Bug Fixes + +* update project readme ([#66](https://github.com/libp2p/js-libp2p-utils/issues/66)) ([7e977a2](https://github.com/libp2p/js-libp2p-utils/commit/7e977a2739717225a4b1d74304e69500652a3386)) + + +### Dependencies + +* bump private-ip from 2.3.4 to 3.0.0 ([#63](https://github.com/libp2p/js-libp2p-utils/issues/63)) ([956f404](https://github.com/libp2p/js-libp2p-utils/commit/956f404bba12f2c712999046b19825496fe8be41)), closes [ChainSafe/is-ip#1](https://github.com/ChainSafe/is-ip/issues/1) [#19](https://github.com/libp2p/js-libp2p-utils/issues/19) [#21](https://github.com/libp2p/js-libp2p-utils/issues/21) +* **dev:** bump it-all from 1.0.6 to 2.0.0 ([#62](https://github.com/libp2p/js-libp2p-utils/issues/62)) ([99cca25](https://github.com/libp2p/js-libp2p-utils/commit/99cca2505721f282ed557dcfd28d8b46d064d6e2)), closes [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#27](https://github.com/libp2p/js-libp2p-utils/issues/27) [#24](https://github.com/libp2p/js-libp2p-utils/issues/24) +* **dev:** bump it-map from 1.0.6 to 2.0.0 ([#61](https://github.com/libp2p/js-libp2p-utils/issues/61)) ([88b05b4](https://github.com/libp2p/js-libp2p-utils/commit/88b05b4a6223774cd6dbaaa4f97e1da318f89856)) +* **dev:** bump uint8arrays from 3.1.1 to 4.0.2 ([#60](https://github.com/libp2p/js-libp2p-utils/issues/60)) ([ca0b632](https://github.com/libp2p/js-libp2p-utils/commit/ca0b63243b0ae23b6fa9195387466516c9acce80)), closes [#41](https://github.com/libp2p/js-libp2p-utils/issues/41) [#40](https://github.com/libp2p/js-libp2p-utils/issues/40) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#41](https://github.com/libp2p/js-libp2p-utils/issues/41) [#40](https://github.com/libp2p/js-libp2p-utils/issues/40) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#41](https://github.com/libp2p/js-libp2p-utils/issues/41) [#40](https://github.com/libp2p/js-libp2p-utils/issues/40) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) + +## [3.0.2](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.1...v3.0.2) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([ea425dc](https://github.com/libp2p/js-libp2p-utils/commit/ea425dc19253202009497587681da1a7703ac429)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#59](https://github.com/libp2p/js-libp2p-utils/issues/59)) ([46bff23](https://github.com/libp2p/js-libp2p-utils/commit/46bff23bc5296359cfca02fbf078ee197a1629cc)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.0...v3.0.1) (2022-08-10) + + +### Bug Fixes + +* update deps ([#55](https://github.com/libp2p/js-libp2p-utils/issues/55)) ([134c633](https://github.com/libp2p/js-libp2p-utils/commit/134c633f107247ce309ed7da3a29f872615ee920)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-utils/compare/v2.0.1...v3.0.0) (2022-06-27) + + +### ⚠ BREAKING CHANGES + +* **deps:** the API of the returned MultiaddrConnection has changed + +### Trivial Changes + +* **deps:** bump @libp2p/interface-connection from 1.0.1 to 2.1.0 ([#51](https://github.com/libp2p/js-libp2p-utils/issues/51)) ([0f99bf8](https://github.com/libp2p/js-libp2p-utils/commit/0f99bf833f7732d74eac4a06fd2b607555c7f34b)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-utils/compare/v2.0.0...v2.0.1) (2022-06-27) + + +### Trivial Changes + +* remove unused deps ([#52](https://github.com/libp2p/js-libp2p-utils/issues/52)) ([8f339c9](https://github.com/libp2p/js-libp2p-utils/commit/8f339c9a50b466d65b41492abfd1bd88ffa0a38c)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.10...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update libp2p interfaces ([#47](https://github.com/libp2p/js-libp2p-utils/issues/47)) ([018fbe4](https://github.com/libp2p/js-libp2p-utils/commit/018fbe48f3506c0b90dc88779a3f12a2714ab09c)) + +### [1.0.10](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.9...v1.0.10) (2022-04-07) + + +### Trivial Changes + +* update aegir ([#39](https://github.com/libp2p/js-libp2p-utils/issues/39)) ([34f1fde](https://github.com/libp2p/js-libp2p-utils/commit/34f1fde4310c6571e90a6d312924146e089b5a9d)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.8...v1.0.9) (2022-03-15) + + +### Bug Fixes + +* refactor address sort to be a regular sort function ([#35](https://github.com/libp2p/js-libp2p-utils/issues/35)) ([8d4e3d6](https://github.com/libp2p/js-libp2p-utils/commit/8d4e3d6f1b56d24e4e58df16ded87d3ca4f82a3f)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.7...v1.0.8) (2022-03-03) + + +### Bug Fixes + +* correct update path for stream-to-ma-conn ([#34](https://github.com/libp2p/js-libp2p-utils/issues/34)) ([90cc6f5](https://github.com/libp2p/js-libp2p-utils/commit/90cc6f563c8640ba52ebfe2f8794999664ccd7eb)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.6...v1.0.7) (2022-03-02) + + +### Bug Fixes + +* pass duplex to stream-to-ma-conn not stream ([#33](https://github.com/libp2p/js-libp2p-utils/issues/33)) ([ebc5c60](https://github.com/libp2p/js-libp2p-utils/commit/ebc5c6074c971e39c6e5c5c51e251aa00c544b50)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.5...v1.0.6) (2022-02-10) + + +### Bug Fixes + +* update interfaces ([#32](https://github.com/libp2p/js-libp2p-utils/issues/32)) ([5d960f3](https://github.com/libp2p/js-libp2p-utils/commit/5d960f3d1566ccb9bf043b71eca5a83117955940)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.4...v1.0.5) (2022-01-15) + + +### Bug Fixes + +* update it-* deps to typed versions ([#30](https://github.com/libp2p/js-libp2p-utils/issues/30)) ([b65ae5c](https://github.com/libp2p/js-libp2p-utils/commit/b65ae5c813efdc20ac11a13f349dc914ffc64c48)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.3...v1.0.4) (2022-01-15) + + +### Trivial Changes + +* engines version ([#29](https://github.com/libp2p/js-libp2p-utils/issues/29)) ([47ce53c](https://github.com/libp2p/js-libp2p-utils/commit/47ce53c34b0106101215ed4b3ffe9f1fffd51cb6)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.2...v1.0.3) (2022-01-14) + + +### Trivial Changes + +* project updates ([#23](https://github.com/libp2p/js-libp2p-utils/issues/23)) ([cb7ea61](https://github.com/libp2p/js-libp2p-utils/commit/cb7ea61e6df7a721863ad8fba7c73d376b2c3ab8)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.1...v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#22](https://github.com/libp2p/js-libp2p-utils/issues/22)) ([5fcf8c7](https://github.com/libp2p/js-libp2p-utils/commit/5fcf8c7f57864e4e92f6606656cf3ea1f214f0c4)) + +## [0.4.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.4.0...v0.4.1) (2021-07-08) + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-utils/compare/v0.3.1...v0.4.0) (2021-07-07) + + +### chore + +* update to new multiformats ([#18](https://github.com/libp2p/js-libp2p-utils/issues/18)) ([24ca72c](https://github.com/libp2p/js-libp2p-utils/commit/24ca72c95bf485af513fba59830796a5fcf71437)) + + +### BREAKING CHANGES + +* updates multiaddr which uses the new CID class + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.3.0...v0.3.1) (2021-04-12) + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-utils/compare/v0.2.3...v0.3.0) (2021-04-08) + + +### Features + +* add types ([#16](https://github.com/libp2p/js-libp2p-utils/issues/16)) ([e0552b5](https://github.com/libp2p/js-libp2p-utils/commit/e0552b5b6b1d912a8f6f1e39b1a4b70fca91f547)) + + + + +## [0.2.3](https://github.com/libp2p/js-libp2p-utils/compare/v0.2.2...v0.2.3) (2020-11-30) + + + + +## [0.2.2](https://github.com/libp2p/js-libp2p-utils/compare/v0.2.1...v0.2.2) (2020-11-16) + + +### Features + +* address sorter ([#13](https://github.com/libp2p/js-libp2p-utils/issues/13)) ([cb5e716](https://github.com/libp2p/js-libp2p-utils/commit/cb5e716)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.3...v0.2.1) (2020-10-08) + + +### Chores + +* update deps ([#9](https://github.com/libp2p/js-libp2p-utils/issues/9)) ([a2ea68f](https://github.com/libp2p/js-libp2p-utils/commit/a2ea68f)) + + +### Features + +* is multiaddr private and loopback ([#10](https://github.com/libp2p/js-libp2p-utils/issues/10)) ([d7fa562](https://github.com/libp2p/js-libp2p-utils/commit/d7fa562)) + + +### BREAKING CHANGES + +* - The multiaddr dep of this module uses Uint8Arrays and may not be + compatible with previous versions + +* chore: remove gh url + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.3...v0.2.0) (2020-08-07) + + +### Chores + +* update deps ([#9](https://github.com/libp2p/js-libp2p-utils/issues/9)) ([a2ea68f](https://github.com/libp2p/js-libp2p-utils/commit/a2ea68f)) + + +### BREAKING CHANGES + +* - The multiaddr dep of this module uses Uint8Arrays and may not be + compatible with previous versions + +* chore: remove gh url + + + + +## [0.1.3](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.2...v0.1.3) (2020-07-15) + + +### Features + +* arrayEquals for non primitive types with equals function ([80668ff](https://github.com/libp2p/js-libp2p-utils/commit/80668ff)) + + + + +## [0.1.2](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.1...v0.1.2) (2020-02-15) + + +### Features + +* stream to multiaddr connection converter ([#2](https://github.com/libp2p/js-libp2p-utils/issues/2)) ([6220631](https://github.com/libp2p/js-libp2p-utils/commit/6220631)) + + + + +## [0.1.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.0...v0.1.1) (2020-02-13) + + + + +# 0.1.0 (2019-09-23) + + +### 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)) diff --git a/packages/utils/LICENSE b/packages/utils/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/utils/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/utils/LICENSE-APACHE b/packages/utils/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/utils/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/utils/LICENSE-MIT b/packages/utils/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/utils/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 0000000000..4fbd0f1a5f --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,65 @@ +# @libp2p/utils + +[![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) + +> Package to aggregate shared logic and dependencies for the libp2p ecosystem + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +The libp2p ecosystem has lots of repos with it comes several problems like: + +- Domain logic dedupe - all modules shared a lot of logic like validation, streams handling, etc. +- Dependencies management - it's really easy with so many repos for dependencies to go out of control, they become outdated, different repos use different modules to do the same thing (like merging defaults options), browser bundles ends up with multiple versions of the same package, bumping versions is cumbersome to do because we need to go through several repos, etc. + +These problems are the motivation for this package, having shared logic in this package avoids creating cyclic dependencies, centralizes common use modules/functions (exactly like aegir does for the tooling), semantic versioning for 3rd party dependencies is handled in one single place (a good example is going from streams 2 to 3) and maintainers should only care about having `libp2p-utils` updated. + +## Usage + +Each function should be imported directly. + +```js +import ipAndPortToMultiaddr from '@libp2p/utils/ip-port-to-multiaddr' + +const ma = ipAndPortToMultiaddr('127.0.0.1', 9000) +``` + +You can check the [API docs](https://libp2p.github.io/js-libp2p-utils). + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000000..604b675c13 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,110 @@ +{ + "name": "@libp2p/utils", + "version": "3.0.12", + "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", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./address-sort": { + "types": "./dist/src/address-sort.d.ts", + "import": "./dist/src/address-sort.js" + }, + "./array-equals": { + "types": "./dist/src/array-equals.d.ts", + "import": "./dist/src/array-equals.js" + }, + "./ip-port-to-multiaddr": { + "types": "./dist/src/ip-port-to-multiaddr.d.ts", + "import": "./dist/src/ip-port-to-multiaddr.js" + }, + "./multiaddr/is-loopback": { + "types": "./dist/src/multiaddr/is-loopback.d.ts", + "import": "./dist/src/multiaddr/is-loopback.js" + }, + "./multiaddr/is-private": { + "types": "./dist/src/multiaddr/is-private.d.ts", + "import": "./dist/src/multiaddr/is-private.js" + }, + "./stream-to-ma-conn": { + "types": "./dist/src/stream-to-ma-conn.d.ts", + "import": "./dist/src/stream-to-ma-conn.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@achingbrain/ip-address": "^8.1.0", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "abortable-iterator": "^5.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": "^39.0.10", + "it-all": "^3.0.1", + "it-pair": "^2.0.6", + "it-pipe": "^3.0.1", + "uint8arrays": "^4.0.3" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/utils/src/address-sort.ts b/packages/utils/src/address-sort.ts new file mode 100644 index 0000000000..3df8ababd4 --- /dev/null +++ b/packages/utils/src/address-sort.ts @@ -0,0 +1,55 @@ +/** + * @packageDocumentation + * + * Provides strategies to sort a list of multiaddrs. + * + * @example + * + * ```typescript + * import { publicAddressesFirst } from '@libp2p/utils/address-sort' + * import { multiaddr } from '@multformats/multiaddr' + * + * + * const addresses = [ + * multiaddr('/ip4/127.0.0.1/tcp/9000'), + * multiaddr('/ip4/82.41.53.1/tcp/9000') + * ].sort(publicAddressesFirst) + * + * console.info(addresses) + * // ['/ip4/82.41.53.1/tcp/9000', '/ip4/127.0.0.1/tcp/9000'] + * ``` + */ + +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. + */ +export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 { + const isAPrivate = isPrivate(a.multiaddr) + const isBPrivate = isPrivate(b.multiaddr) + + if (isAPrivate && !isBPrivate) { + return 1 + } else if (!isAPrivate && isBPrivate) { + return -1 + } + // Check certified? + if (a.isCertified && !b.isCertified) { + return -1 + } else if (!a.isCertified && b.isCertified) { + return 1 + } + + return 0 +} + +/** + * A test thing + */ +export async function something (): Promise { + return Uint8Array.from([0, 1, 2]) +} diff --git a/packages/utils/src/array-equals.ts b/packages/utils/src/array-equals.ts new file mode 100644 index 0000000000..e94499acbb --- /dev/null +++ b/packages/utils/src/array-equals.ts @@ -0,0 +1,34 @@ +/** + * @packageDocumentation + * + * Provides strategies ensure arrays are equivalent. + * + * @example + * + * ```typescript + * import { arrayEquals } from '@libp2p/utils/array-equals' + * import { multiaddr } from '@multformats/multiaddr' + * + * const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9000'), + * const ma2 = multiaddr('/ip4/82.41.53.1/tcp/9000') + * + * console.info(arrayEquals([ma1], [ma1])) // true + * console.info(arrayEquals([ma1], [ma2])) // false + * ``` + */ + +/** + * Verify if two arrays of non primitive types with the "equals" function are equal. + * Compatible with multiaddr, peer-id and others. + */ +export function arrayEquals (a: any[], b: any[]): boolean { + const sort = (a: any, b: any): number => a.toString().localeCompare(b.toString()) + + if (a.length !== b.length) { + return false + } + + b.sort(sort) + + return a.sort(sort).every((item, index) => b[index].equals(item)) +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/utils/src/ip-port-to-multiaddr.ts b/packages/utils/src/ip-port-to-multiaddr.ts new file mode 100644 index 0000000000..bff7020e3a --- /dev/null +++ b/packages/utils/src/ip-port-to-multiaddr.ts @@ -0,0 +1,47 @@ +import { Address4, Address6 } from '@achingbrain/ip-address' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { type Multiaddr, multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:ip-port-to-multiaddr') + +export const Errors = { + ERR_INVALID_IP_PARAMETER: 'ERR_INVALID_IP_PARAMETER', + ERR_INVALID_PORT_PARAMETER: 'ERR_INVALID_PORT_PARAMETER', + ERR_INVALID_IP: 'ERR_INVALID_IP' +} + +/** + * Transform an IP, Port pair into a multiaddr + */ +export function ipPortToMultiaddr (ip: string, port: number | string): Multiaddr { + if (typeof ip !== 'string') { + throw new CodeError(`invalid ip provided: ${ip}`, Errors.ERR_INVALID_IP_PARAMETER) // eslint-disable-line @typescript-eslint/restrict-template-expressions + } + + if (typeof port === 'string') { + port = parseInt(port) + } + + if (isNaN(port)) { + throw new CodeError(`invalid port provided: ${port}`, Errors.ERR_INVALID_PORT_PARAMETER) + } + + try { + // Test valid IPv4 + new Address4(ip) // eslint-disable-line no-new + return multiaddr(`/ip4/${ip}/tcp/${port}`) + } catch {} + + try { + // Test valid IPv6 + const ip6 = new Address6(ip) + return ip6.is4() + ? multiaddr(`/ip4/${ip6.to4().correctForm()}/tcp/${port}`) + : multiaddr(`/ip6/${ip}/tcp/${port}`) + } catch (err) { + const errMsg = `invalid ip:port for creating a multiaddr: ${ip}:${port}` + log.error(errMsg) + throw new CodeError(errMsg, Errors.ERR_INVALID_IP) + } +} diff --git a/packages/utils/src/multiaddr/is-loopback.ts b/packages/utils/src/multiaddr/is-loopback.ts new file mode 100644 index 0000000000..d66f545681 --- /dev/null +++ b/packages/utils/src/multiaddr/is-loopback.ts @@ -0,0 +1,11 @@ +import { isLoopbackAddr } from 'is-loopback-addr' +import type { Multiaddr } from '@multiformats/multiaddr' + +/** + * Check if a given multiaddr is a loopback address. + */ +export function isLoopback (ma: Multiaddr): boolean { + const { address } = ma.nodeAddress() + + return isLoopbackAddr(address) +} diff --git a/packages/utils/src/multiaddr/is-private.ts b/packages/utils/src/multiaddr/is-private.ts new file mode 100644 index 0000000000..ab563e032c --- /dev/null +++ b/packages/utils/src/multiaddr/is-private.ts @@ -0,0 +1,15 @@ +import isIpPrivate from 'private-ip' +import type { Multiaddr } from '@multiformats/multiaddr' + +/** + * Check if a given multiaddr has a private address. + */ +export function isPrivate (ma: Multiaddr): boolean { + try { + const { address } = ma.nodeAddress() + + return Boolean(isIpPrivate(address)) + } catch { + return true + } +} diff --git a/packages/utils/src/stream-to-ma-conn.ts b/packages/utils/src/stream-to-ma-conn.ts new file mode 100644 index 0000000000..3fa09dac4a --- /dev/null +++ b/packages/utils/src/stream-to-ma-conn.ts @@ -0,0 +1,94 @@ +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Duplex, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +const log = logger('libp2p:stream:converter') + +export interface Timeline { + /** + * Connection opening timestamp + */ + open: number + + /** + * Connection upgraded timestamp + */ + upgraded?: number + + /** + * Connection closed timestamp + */ + close?: number +} + +export interface StreamOptions { + signal?: AbortSignal + +} + +export interface StreamProperties { + stream: Duplex, Source> + remoteAddr: Multiaddr + localAddr: Multiaddr +} + +/** + * Convert a duplex iterable into a MultiaddrConnection. + * https://github.com/libp2p/interface-transport#multiaddrconnection + */ +export function streamToMaConnection (props: StreamProperties, options: StreamOptions = {}): MultiaddrConnection { + const { stream, remoteAddr } = props + const { sink, source } = stream + + const mapSource = (async function * () { + for await (const list of source) { + if (list instanceof Uint8Array) { + yield list + } else { + yield * list + } + } + }()) + + const maConn: MultiaddrConnection = { + async sink (source) { + if (options.signal != null) { + source = abortableSource(source, options.signal) + } + + try { + await sink(source) + await close() + } catch (err: any) { + // If aborted we can safely ignore + if (err.type !== 'aborted') { + // If the source errored the socket will already have been destroyed by + // toIterable.duplex(). If the socket errored it will already be + // destroyed. There's nothing to do here except log the error & return. + log(err) + } + } + }, + source: (options.signal != null) ? abortableSource(mapSource, options.signal) : mapSource, + remoteAddr, + timeline: { open: Date.now(), close: undefined }, + async close () { + await sink(async function * () { + yield new Uint8Array(0) + }()) + await close() + } + } + + async function close (): Promise { + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + await Promise.resolve() + } + + return maConn +} diff --git a/packages/utils/test/address-sort.spec.ts b/packages/utils/test/address-sort.spec.ts new file mode 100644 index 0000000000..39ede62c9a --- /dev/null +++ b/packages/utils/test/address-sort.spec.ts @@ -0,0 +1,51 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { publicAddressesFirst } from '../src/address-sort.js' + +describe('address-sort', () => { + it('should sort public addresses first', () => { + const addresses = [ + { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/31.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) + }) + + it('should sort public certified addresses first', () => { + const addresses = [ + { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'), + isCertified: true + } + ] + + 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) + }) +}) diff --git a/packages/utils/test/array-equals.spec.ts b/packages/utils/test/array-equals.spec.ts new file mode 100644 index 0000000000..7b50b76071 --- /dev/null +++ b/packages/utils/test/array-equals.spec.ts @@ -0,0 +1,70 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { arrayEquals } from '../src/array-equals.js' + +describe('non primitive array equals', () => { + it('returns true if two arrays of multiaddrs are equal', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(true) + }) + + it('returns true if two arrays of multiaddrs have the same content but different orders', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(true) + }) + + it('returns false if two arrays of multiaddrs are different', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/8001'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(false) + }) + + it('returns false if two arrays of multiaddrs are partially equal, but different lengths', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(false) + }) +}) diff --git a/packages/utils/test/ip-port-to-multiaddr.spec.ts b/packages/utils/test/ip-port-to-multiaddr.spec.ts new file mode 100644 index 0000000000..2e1631743f --- /dev/null +++ b/packages/utils/test/ip-port-to-multiaddr.spec.ts @@ -0,0 +1,47 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { ipPortToMultiaddr, Errors } from '../src/ip-port-to-multiaddr.js' + +describe('IP and port to Multiaddr', () => { + it('creates multiaddr from valid IPv4 IP and port', () => { + const ip = '127.0.0.1' + const port = '9090' + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip4/${ip}/tcp/${port}`) + }) + + it('creates multiaddr from valid IPv4 IP and numeric port', () => { + const ip = '127.0.0.1' + const port = 9090 + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip4/${ip}/tcp/${port}`) + }) + + it('creates multiaddr from valid IPv4 in IPv6 IP and port', () => { + const ip = '0:0:0:0:0:0:101.45.75.219' + const port = '9090' + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip4/101.45.75.219/tcp/${port}`) + }) + + it('creates multiaddr from valid IPv6 IP and port', () => { + const ip = '::1' + const port = '9090' + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip6/${ip}/tcp/${port}`) + }) + + it('throws for missing IP address', () => { + // @ts-expect-error invalid args + expect(() => ipPortToMultiaddr()).to.throw('invalid ip provided').with.property('code', Errors.ERR_INVALID_IP_PARAMETER) + }) + + it('throws for invalid IP address', () => { + const ip = 'aewmrn4awoew' + const port = '234' + expect(() => ipPortToMultiaddr(ip, port)).to.throw('invalid ip:port for creating a multiaddr').with.property('code', Errors.ERR_INVALID_IP) + }) + + it('throws for invalid port', () => { + const ip = '127.0.0.1' + const port = 'garbage' + expect(() => ipPortToMultiaddr(ip, port)).to.throw('invalid port provided').with.property('code', Errors.ERR_INVALID_PORT_PARAMETER) + }) +}) diff --git a/packages/utils/test/multiaddr/is-loopback.spec.ts b/packages/utils/test/multiaddr/is-loopback.spec.ts new file mode 100644 index 0000000000..3aa79ca6cb --- /dev/null +++ b/packages/utils/test/multiaddr/is-loopback.spec.ts @@ -0,0 +1,55 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { isLoopback } from '../../src/multiaddr/is-loopback.js' + +describe('multiaddr isLoopback', () => { + it('identifies loopback ip4 multiaddrs', () => { + [ + multiaddr('/ip4/127.0.0.1/tcp/1000'), + multiaddr('/ip4/127.0.1.1/tcp/1000'), + multiaddr('/ip4/127.1.1.1/tcp/1000'), + multiaddr('/ip4/127.255.255.255/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(true) + }) + }) + + it('identifies non loopback ip4 multiaddrs', () => { + [ + multiaddr('/ip4/101.0.26.90/tcp/1000'), + multiaddr('/ip4/10.0.0.1/tcp/1000'), + multiaddr('/ip4/192.168.0.1/tcp/1000'), + multiaddr('/ip4/172.16.0.1/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(false) + }) + }) + + it('identifies loopback ip6 multiaddrs', () => { + [ + multiaddr('/ip6/::1/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(true) + }) + }) + + it('identifies non loopback ip6 multiaddrs', () => { + [ + multiaddr('/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/1000'), + multiaddr('/ip6/::/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(false) + }) + }) + + it('identifies other multiaddrs as not loopback addresses', () => { + [ + multiaddr('/dns4/wss0.bootstrap.libp2p.io/tcp/443'), + multiaddr('/dns6/wss0.bootstrap.libp2p.io/tcp/443') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(false) + }) + }) +}) diff --git a/packages/utils/test/multiaddr/is-private.spec.ts b/packages/utils/test/multiaddr/is-private.spec.ts new file mode 100644 index 0000000000..a5a12f6406 --- /dev/null +++ b/packages/utils/test/multiaddr/is-private.spec.ts @@ -0,0 +1,66 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { isPrivate } from '../../src/multiaddr/is-private.js' + +describe('multiaddr isPrivate', () => { + it('identifies private ip4 multiaddrs', () => { + [ + multiaddr('/ip4/127.0.0.1/tcp/1000'), + multiaddr('/ip4/10.0.0.1/tcp/1000'), + multiaddr('/ip4/192.168.0.1/tcp/1000'), + multiaddr('/ip4/172.16.0.1/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(true) + }) + }) + + it('identifies public ip4 multiaddrs', () => { + [ + multiaddr('/ip4/101.0.26.90/tcp/1000'), + multiaddr('/ip4/40.1.20.9/tcp/1000'), + multiaddr('/ip4/92.168.0.1/tcp/1000'), + multiaddr('/ip4/2.16.0.1/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(false) + }) + }) + + it('identifies private ip6 multiaddrs', () => { + [ + multiaddr('/ip6/fd52:8342:fc46:6c91:3ac9:86ff:fe31:7095/tcp/1000'), + multiaddr('/ip6/fd52:8342:fc46:6c91:3ac9:86ff:fe31:1/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(true) + }) + }) + + it('identifies public ip6 multiaddrs', () => { + [ + multiaddr('/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/1000'), + multiaddr('/ip6/2000:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(false) + }) + }) + + it('identifies other multiaddrs as not private addresses', () => { + [ + multiaddr('/dns4/wss0.bootstrap.libp2p.io/tcp/443'), + multiaddr('/dns6/wss0.bootstrap.libp2p.io/tcp/443') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(false) + }) + }) + + it('identifies non-public addresses', () => { + [ + multiaddr('/ip4/127.0.0.1/tcp/1000/p2p-circuit'), + multiaddr('/unix/foo/bar/baz.sock'), + multiaddr('/ip4/127.0.0.1/sctp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(true) + }) + }) +}) diff --git a/packages/utils/test/stream-to-ma-conn.spec.ts b/packages/utils/test/stream-to-ma-conn.spec.ts new file mode 100644 index 0000000000..470cb85090 --- /dev/null +++ b/packages/utils/test/stream-to-ma-conn.spec.ts @@ -0,0 +1,79 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pair } from 'it-pair' +import { pipe } from 'it-pipe' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { streamToMaConnection } from '../src/stream-to-ma-conn.js' +import type { Stream } from '@libp2p/interface-connection' +import type { Duplex, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +function toMuxedStream (stream: Duplex, Source, Promise>): Stream { + const muxedStream: Stream = { + ...stream, + close: () => {}, + closeRead: () => {}, + closeWrite: () => {}, + abort: () => {}, + reset: () => {}, + stat: { + direction: 'outbound', + timeline: { + open: Date.now() + } + }, + metadata: {}, + id: `muxed-stream-${Math.random()}` + } + + return muxedStream +} + +describe('Convert stream into a multiaddr connection', () => { + const localAddr = multiaddr('/ip4/101.45.75.219/tcp/6000') + const remoteAddr = multiaddr('/ip4/100.46.74.201/tcp/6002') + + it('converts a stream and adds the provided metadata', async () => { + const stream = pair() + + const maConn = streamToMaConnection({ + stream: toMuxedStream(stream), + localAddr, + remoteAddr + }) + + expect(maConn).to.exist() + expect(maConn.sink).to.exist() + expect(maConn.source).to.exist() + expect(maConn.remoteAddr).to.eql(remoteAddr) + expect(maConn.timeline).to.exist() + expect(maConn.timeline.open).to.exist() + expect(maConn.timeline.close).to.not.exist() + + await maConn.close() + expect(maConn.timeline.close).to.exist() + }) + + it('can stream data over the multiaddr connection', async () => { + const stream = pair() + const maConn = streamToMaConnection({ + stream: toMuxedStream(stream), + localAddr, + remoteAddr + }) + + const data = uint8ArrayFromString('hey') + const streamData = await pipe( + [data], + maConn, + async (source) => all(source) + ) + + expect(streamData).to.eql([data]) + // underlying stream end closes the connection + expect(maConn.timeline.close).to.exist() + }) +}) diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000000..1bd7fbd5b8 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + } + ] +}