From cee5c38a294e3411f732e6d93118ca129f3466fa Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 9 Jun 2022 19:25:57 +0100 Subject: [PATCH] chore: switch to ts version --- .github/dependabot.yml | 8 + .github/workflows/automerge.yml | 8 + .github/workflows/js-test-and-release.yml | 152 +++++ .gitignore | 41 +- .travis.yml | 43 -- CHANGELOG.md | 232 +++++--- LICENSE | 23 +- LICENSE-APACHE | 5 + LICENSE-MIT | 19 + README.md | 227 +------ package.json | 257 ++++++-- src/errors.ts | 53 ++ src/index.js | 374 ------------ src/index.ts | 686 ++++++++++++++++++++++ src/message/index.js | 14 - src/message/rpc.proto.js | 20 - src/message/sign.js | 86 --- src/message/topic-descriptor.proto.js | 30 - src/peer-streams.ts | 174 ++++++ src/peer.js | 202 ------- src/sign.ts | 95 +++ src/utils.js | 115 ---- src/utils.ts | 107 ++++ test/emit-self.spec.ts | 108 ++++ test/instance.spec.js | 72 --- test/instance.spec.ts | 42 ++ test/lifecycle.spec.ts | 261 ++++++++ test/message.spec.ts | 91 +++ test/message/rpc.proto | 52 ++ test/message/rpc.ts | 215 +++++++ test/pubsub.spec.js | 355 ----------- test/pubsub.spec.ts | 505 ++++++++++++++++ test/sign.spec.js | 95 --- test/sign.spec.ts | 123 ++++ test/topic-validators.spec.ts | 110 ++++ test/utils.spec.js | 78 --- test/utils.spec.ts | 89 +++ test/utils/index.js | 101 ---- test/utils/index.ts | 158 +++++ tsconfig.json | 13 + 40 files changed, 3435 insertions(+), 2004 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/automerge.yml create mode 100644 .github/workflows/js-test-and-release.yml delete mode 100644 .travis.yml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 src/errors.ts delete mode 100644 src/index.js create mode 100644 src/index.ts delete mode 100644 src/message/index.js delete mode 100644 src/message/rpc.proto.js delete mode 100644 src/message/sign.js delete mode 100644 src/message/topic-descriptor.proto.js create mode 100644 src/peer-streams.ts delete mode 100644 src/peer.js create mode 100644 src/sign.ts delete mode 100644 src/utils.js create mode 100644 src/utils.ts create mode 100644 test/emit-self.spec.ts delete mode 100644 test/instance.spec.js create mode 100644 test/instance.spec.ts create mode 100644 test/lifecycle.spec.ts create mode 100644 test/message.spec.ts create mode 100644 test/message/rpc.proto create mode 100644 test/message/rpc.ts delete mode 100644 test/pubsub.spec.js create mode 100644 test/pubsub.spec.ts delete mode 100644 test/sign.spec.js create mode 100644 test/sign.spec.ts create mode 100644 test/topic-validators.spec.ts delete mode 100644 test/utils.spec.js create mode 100644 test/utils.spec.ts delete mode 100644 test/utils/index.js create mode 100644 test/utils/index.ts create mode 100644 tsconfig.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..290ad02837 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "10:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000000..d57c2a02b6 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,8 @@ +name: Automerge +on: [ pull_request ] + +jobs: + automerge: + uses: protocol/.github/.github/workflows/automerge.yml@master + with: + job: 'automerge' diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml new file mode 100644 index 0000000000..f3f9e72b8e --- /dev/null +++ b/.github/workflows/js-test-and-release.yml @@ -0,0 +1,152 @@ +name: test & maybe release +on: + push: + branches: + - master # with #262 - ${{{ github.default_branch }}} + pull_request: + branches: + - master # with #262 - ${{{ github.default_branch }}} + +jobs: + + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present lint + - run: npm run --if-present dep-check + + test-node: + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [16] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:node + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: node + + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: chrome + + test-chrome-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome-webworker + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: chrome-webworker + + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: firefox + + test-firefox-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox-webworker + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: firefox-webworker + + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-main + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: electron-main + + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-renderer + - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0 + with: + directory: ./.nyc_output + flags: electron-renderer + + release: + needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main, test-electron-renderer] + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' # with #262 - 'refs/heads/${{{ github.default_branch }}}' + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - uses: ipfs/aegir/actions/docker-login@master + with: + docker-token: ${{ secrets.DOCKER_TOKEN }} + docker-username: ${{ secrets.DOCKER_USERNAME }} + - run: npm run --if-present release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 649e624d2e..db79d1f189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,40 +1,7 @@ -docs -**/node_modules/ -**/*.log -test/repo-tests* - -# Logs -logs -*.log - -coverage - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -build - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules - -dist - -docs - +coverage +.nyc_output package-lock.json yarn.lock +docs +dist diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0c93b87494..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: node_js -cache: npm -stages: - - check - - test - - cov - -node_js: - - '10' - -os: - - linux - - osx - - windows - -script: npx nyc -s npm run test:node -- --bail -after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov - -jobs: - include: - - stage: check - script: - - npx aegir dep-check - - npm run lint - - - stage: test - name: chrome - addons: - chrome: stable - script: - - npx aegir test -t browser - - npx aegir test -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: - - npx aegir test -t browser -- --browsers FirefoxHeadless - - npx aegir test -t webworker -- --browsers FirefoxHeadless - -notifications: - email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index c80bf4d0e5..4d364979e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,250 +1,306 @@ - -# [0.6.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.6...v0.6.0) (2020-08-11) +## [@libp2p/pubsub-v1.3.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.24...@libp2p/pubsub-v1.3.0) (2022-05-23) + + +### Features + +* expose utility methods to convert bigint to bytes and back ([#213](https://github.com/libp2p/js-libp2p-interfaces/issues/213)) ([3d2e59c](https://github.com/libp2p/js-libp2p-interfaces/commit/3d2e59c8fd8af5d618df904ae9d40518a13de547)) + +## [@libp2p/pubsub-v1.2.24](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.23...@libp2p/pubsub-v1.2.24) (2022-05-20) ### Bug Fixes -* replace node buffers with uint8arrays ([#70](https://github.com/libp2p/js-libp2p-pubsub/issues/70)) ([92632b5](https://github.com/libp2p/js-libp2p-pubsub/commit/92632b5)) +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) +## [@libp2p/pubsub-v1.2.23](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.22...@libp2p/pubsub-v1.2.23) (2022-05-10) -### BREAKING CHANGES -* - The `.data`, `.from` and `.seq` properties of messages used to be - node Buffers, now they are Uint8Arrays -- All deps of this module now use Uint8Arrays instead of Buffers +### 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/pubsub-v1.2.22](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.21...@libp2p/pubsub-v1.2.22) (2022-05-10) - -## [0.5.6](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.5...v0.5.6) (2020-07-14) +### 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)) - -## [0.5.5](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.4...v0.5.5) (2020-07-14) +## [@libp2p/pubsub-v1.2.21](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.20...@libp2p/pubsub-v1.2.21) (2022-05-04) +### Bug Fixes - -## [0.5.4](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.3...v0.5.4) (2020-07-07) +* 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/pubsub-v1.2.20](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.19...@libp2p/pubsub-v1.2.20) (2022-04-22) ### Bug Fixes -* quick reconnects ([#57](https://github.com/libp2p/js-libp2p-pubsub/issues/57)) ([f86dfbc](https://github.com/libp2p/js-libp2p-pubsub/commit/f86dfbc)) +* update pubsub interface in line with gossipsub ([#199](https://github.com/libp2p/js-libp2p-interfaces/issues/199)) ([3f55596](https://github.com/libp2p/js-libp2p-interfaces/commit/3f555965cddea3ef03e7217b755c82aa4107e093)) + +## [@libp2p/pubsub-v1.2.19](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.18...@libp2p/pubsub-v1.2.19) (2022-04-21) +### Bug Fixes + +* test PubSub interface and not PubSubBaseProtocol ([#198](https://github.com/libp2p/js-libp2p-interfaces/issues/198)) ([96c15c9](https://github.com/libp2p/js-libp2p-interfaces/commit/96c15c9780821a3cb763e48854d64377bf562692)) - -## [0.5.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.2...v0.5.3) (2020-06-15) +## [@libp2p/pubsub-v1.2.18](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.17...@libp2p/pubsub-v1.2.18) (2022-04-20) ### Bug Fixes -* not create stream if it already exists ([#48](https://github.com/libp2p/js-libp2p-pubsub/issues/48)) ([f3b06d9](https://github.com/libp2p/js-libp2p-pubsub/commit/f3b06d9)) +* emit pubsub messages using 'message' event ([#197](https://github.com/libp2p/js-libp2p-interfaces/issues/197)) ([df9b685](https://github.com/libp2p/js-libp2p-interfaces/commit/df9b685cea30653109f2fa2cb5583a3bca7b09bb)) +## [@libp2p/pubsub-v1.2.17](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.16...@libp2p/pubsub-v1.2.17) (2022-04-19) - -## [0.5.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.1...v0.5.2) (2020-06-04) +### Trivial Changes + +* remove extraneous readme ([#196](https://github.com/libp2p/js-libp2p-interfaces/issues/196)) ([ee1d00c](https://github.com/libp2p/js-libp2p-interfaces/commit/ee1d00cc209909836f12f17d62f1165f11689488)) + +## [@libp2p/pubsub-v1.2.16](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.15...@libp2p/pubsub-v1.2.16) (2022-04-19) ### Bug Fixes -* use unidirectional streams ([#45](https://github.com/libp2p/js-libp2p-pubsub/issues/45)) ([c6ba48d](https://github.com/libp2p/js-libp2p-pubsub/commit/c6ba48d)) +* 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/pubsub-v1.2.15](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.14...@libp2p/pubsub-v1.2.15) (2022-04-13) +### Bug Fixes + +* add keychain types, fix bigint types ([#193](https://github.com/libp2p/js-libp2p-interfaces/issues/193)) ([9ceadf9](https://github.com/libp2p/js-libp2p-interfaces/commit/9ceadf9d5c42a12d88d74ddd9140e34f7fa63537)) - -## [0.5.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.5.0...v0.5.1) (2020-04-23) +## [@libp2p/pubsub-v1.2.14](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.13...@libp2p/pubsub-v1.2.14) (2022-04-08) ### Bug Fixes -* remove node globals ([#42](https://github.com/libp2p/js-libp2p-pubsub/issues/42)) ([636041b](https://github.com/libp2p/js-libp2p-pubsub/commit/636041b)) +* 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 - -# [0.5.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.4.3...v0.5.0) (2020-04-22) +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) +## [@libp2p/pubsub-v1.2.13](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.12...@libp2p/pubsub-v1.2.13) (2022-03-24) -### Chores -* remove peer-info usage ([21a63cb](https://github.com/libp2p/js-libp2p-pubsub/commit/21a63cb)) +### 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)) -### BREAKING CHANGES +## [@libp2p/pubsub-v1.2.12](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.11...@libp2p/pubsub-v1.2.12) (2022-03-21) -* pubsub internal peer does not have info propery anymore and use the new topology api with peer-id instead of peer-info +### Bug Fixes +* handle empty pubsub messages ([#185](https://github.com/libp2p/js-libp2p-interfaces/issues/185)) ([0db8d84](https://github.com/libp2p/js-libp2p-interfaces/commit/0db8d84dd98ff6e99776c01a6b5bab404033bffa)) - -## [0.4.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.4.1...v0.4.3) (2020-02-14) +## [@libp2p/pubsub-v1.2.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.10...@libp2p/pubsub-v1.2.11) (2022-03-20) ### Bug Fixes -* remove use of assert module ([#37](https://github.com/libp2p/js-libp2p-pubsub/issues/37)) ([d452054](https://github.com/libp2p/js-libp2p-pubsub/commit/d452054)) +* update pubsub types ([#183](https://github.com/libp2p/js-libp2p-interfaces/issues/183)) ([7ef4baa](https://github.com/libp2p/js-libp2p-interfaces/commit/7ef4baad0fe30f783f3eecd5199ef92af08b7f57)) + +## [@libp2p/pubsub-v1.2.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.9...@libp2p/pubsub-v1.2.10) (2022-03-15) +### Bug Fixes - -## [0.4.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.4.1...v0.4.2) (2020-02-02) +* 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/pubsub-v1.2.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.8...@libp2p/pubsub-v1.2.9) (2022-02-27) - -## [0.4.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.4.0...v0.4.1) (2020-01-07) +### 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/pubsub-v1.2.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.7...@libp2p/pubsub-v1.2.8) (2022-02-27) ### Bug Fixes -* catch newStream errors ([#34](https://github.com/libp2p/js-libp2p-pubsub/issues/34)) ([57453d4](https://github.com/libp2p/js-libp2p-pubsub/commit/57453d4)) +* 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/pubsub-v1.2.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.6...@libp2p/pubsub-v1.2.7) (2022-02-18) - -# [0.4.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.3.2...v0.4.0) (2019-12-01) +### Bug Fixes +* simpler pubsub ([#172](https://github.com/libp2p/js-libp2p-interfaces/issues/172)) ([98715ed](https://github.com/libp2p/js-libp2p-interfaces/commit/98715ed73183b32e4fda3d878a462389548358d9)) -### Chores +## [@libp2p/pubsub-v1.2.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.5...@libp2p/pubsub-v1.2.6) (2022-02-17) -* getSubscribers ([#32](https://github.com/libp2p/js-libp2p-pubsub/issues/32)) ([b76451e](https://github.com/libp2p/js-libp2p-pubsub/commit/b76451e)) +### Bug Fixes -### BREAKING CHANGES +* update deps ([#171](https://github.com/libp2p/js-libp2p-interfaces/issues/171)) ([d0d2564](https://github.com/libp2p/js-libp2p-interfaces/commit/d0d2564a84a0722ab587a3aa6ec01e222442b100)) + +## [@libp2p/pubsub-v1.2.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.4...@libp2p/pubsub-v1.2.5) (2022-02-17) -* getPeersSubscribed renamed to getSubscribers to remove redundant wording +### 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)) - -## [0.3.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.3.1...v0.3.2) (2019-11-28) +## [@libp2p/pubsub-v1.2.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.3...@libp2p/pubsub-v1.2.4) (2022-02-12) ### Bug Fixes -* reduce seqno to 8 bytes ([#31](https://github.com/libp2p/js-libp2p-pubsub/issues/31)) ([d26a19c](https://github.com/libp2p/js-libp2p-pubsub/commit/d26a19c)) +* 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/pubsub-v1.2.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.2...@libp2p/pubsub-v1.2.3) (2022-02-11) - -## [0.3.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.3.0...v0.3.1) (2019-11-15) +### 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/pubsub-v1.2.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.1...@libp2p/pubsub-v1.2.2) (2022-02-10) ### Bug Fixes -* incoming stream conn ([#30](https://github.com/libp2p/js-libp2p-pubsub/issues/30)) ([1b2af2c](https://github.com/libp2p/js-libp2p-pubsub/commit/1b2af2c)) +* make registrar simpler ([#163](https://github.com/libp2p/js-libp2p-interfaces/issues/163)) ([d122f3d](https://github.com/libp2p/js-libp2p-interfaces/commit/d122f3daaccc04039d90814960da92b513265644)) + +## [@libp2p/pubsub-v1.2.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.0...@libp2p/pubsub-v1.2.1) (2022-02-10) +### Bug Fixes - -# [0.3.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.2.1...v0.3.0) (2019-11-14) +* 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/pubsub-v1.2.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.1.0...@libp2p/pubsub-v1.2.0) (2022-02-09) -### Code Refactoring -* async ([#26](https://github.com/libp2p/js-libp2p-pubsub/issues/26)) ([c690b29](https://github.com/libp2p/js-libp2p-pubsub/commit/c690b29)) +### 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)) -### BREAKING CHANGES +## [@libp2p/pubsub-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.6...@libp2p/pubsub-v1.1.0) (2022-02-07) -* Switch to using async/await and async iterators. +### Features +* add logger package ([#158](https://github.com/libp2p/js-libp2p-interfaces/issues/158)) ([f327cd2](https://github.com/libp2p/js-libp2p-interfaces/commit/f327cd24825d9ce2f45a02fdb9b47c9735c847e0)) - -## [0.2.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.2.0...v0.2.1) (2019-09-26) +## [@libp2p/pubsub-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.5...@libp2p/pubsub-v1.0.6) (2022-02-05) ### Bug Fixes -* add gossipsub implementation in README ([2684e36](https://github.com/libp2p/js-libp2p-pubsub/commit/2684e36)) -* typo in README ([929ec61](https://github.com/libp2p/js-libp2p-pubsub/commit/929ec61)) +* fix muxer tests ([#157](https://github.com/libp2p/js-libp2p-interfaces/issues/157)) ([7233c44](https://github.com/libp2p/js-libp2p-interfaces/commit/7233c4438479dff56a682f45209ef7a938d63857)) +## [@libp2p/pubsub-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.4...@libp2p/pubsub-v1.0.5) (2022-01-15) -### Features -* allow inline public keys in messages ([3b3fcea](https://github.com/libp2p/js-libp2p-pubsub/commit/3b3fcea)) +### Bug Fixes + +* remove abort controller dep ([#151](https://github.com/libp2p/js-libp2p-interfaces/issues/151)) ([518bce1](https://github.com/libp2p/js-libp2p-interfaces/commit/518bce1f9bd1f8b2922338e0c65c9934af7da3af)) + +## [@libp2p/pubsub-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.3...@libp2p/pubsub-v1.0.4) (2022-01-15) +### Trivial Changes - -# [0.2.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.1.0...v0.2.0) (2019-07-08) +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/pubsub-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.2...@libp2p/pubsub-v1.0.3) (2022-01-14) ### Bug Fixes -* use strict signing properly and fix callback issue ([ca99ce9](https://github.com/libp2p/js-libp2p-pubsub/commit/ca99ce9)) +* update it-* deps to ts versions ([#148](https://github.com/libp2p/js-libp2p-interfaces/issues/148)) ([7a6fdd7](https://github.com/libp2p/js-libp2p-interfaces/commit/7a6fdd7622ce2870b89dbb849ab421d0dd714b43)) +## [@libp2p/pubsub-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.1...@libp2p/pubsub-v1.0.2) (2022-01-08) -### Features -* add validate method for validating signatures ([c36fefa](https://github.com/libp2p/js-libp2p-pubsub/commit/c36fefa)) +### 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.1.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.0.4...v0.1.0) (2019-05-07) +# 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 support for message signing ([5cb17fd](https://github.com/libp2p/js-libp2p-pubsub/commit/5cb17fd)) +* 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 -* as .publish should now sign messages (via _buildMessage) it now requires a callback since signing is async. This also adds an options param to the pubsub constructor to allow for disabling signing. While this change shouldnt break things upstream, implementations need to be sure to call _buildMessage for each message they will publish. +* 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.0.4](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.0.3...v0.0.4) (2019-04-22) +## [0.9.1](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-pubsub@0.9.0...libp2p-pubsub@0.9.1) (2022-01-02) - -## [0.0.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.0.2...v0.0.3) (2019-04-17) +**Note:** Version bump only for package libp2p-pubsub -### Bug Fixes -* align topicid protobuf variable names ([f9a27d7](https://github.com/libp2p/js-libp2p-pubsub/commit/f9a27d7)) -* libp2p crypto for linting ([b654c37](https://github.com/libp2p/js-libp2p-pubsub/commit/b654c37)) + + +# [0.9.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-pubsub@0.8.0...libp2p-pubsub@0.9.0) (2022-01-02) ### Features -* added libp2p-crypto and bs58 dependencies ([c759f38](https://github.com/libp2p/js-libp2p-pubsub/commit/c759f38)) -* added utils.js from js-libp2p-floodsub ([d83e357](https://github.com/libp2p/js-libp2p-pubsub/commit/d83e357)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) - -## [0.0.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v0.0.1...v0.0.2) (2019-02-08) -### Features +# [0.8.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-pubsub@0.7.0...libp2p-pubsub@0.8.0) (2021-12-02) -* added a time cache and a mapping of topics to peers ([13a56a4](https://github.com/libp2p/js-libp2p-pubsub/commit/13a56a4)) +### chore +* update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) - -## 0.0.1 (2019-01-25) + +### BREAKING CHANGES + +* requires node 15+ -### Bug Fixes -* code review ([7ca7f06](https://github.com/libp2p/js-libp2p-pubsub/commit/7ca7f06)) + + +# 0.7.0 (2021-11-22) ### Features -* initial implementation ([a68dc87](https://github.com/libp2p/js-libp2p-pubsub/commit/a68dc87)) +* 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/LICENSE b/LICENSE index 58b2056933..20ce483c86 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,4 @@ -The MIT License (MIT) +This project is dual licensed under MIT and Apache-2.0. -Copyright (c) 2019 Protocol Labs, Inc. - -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. \ No newline at end of file +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/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/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/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/README.md b/README.md index f0b6d27a5e..e59715ffb9 100644 --- a/README.md +++ b/README.md @@ -1,227 +1,36 @@ -# ⛔️ DEPRECATED +# libp2p-pubsub -libp2p-pubsub was refactored and is now in [libp2p/js-libp2p-interfaces/src/pubsub](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/pubsub). If you want to create a libp2p compatible pubsub router you should use the interface from `libp2p@0.29`. +[![test & maybe release](https://github.com/libp2p/js-libp2p-pubsub/actions/workflows/js-test-and-release.yml/badge.svg)](https://github.com/libp2p/js-libp2p-pubsub/actions/workflows/js-test-and-release.yml) -_This library will not be maintained._ +> Contains an implementation of the Pubsub interface -js-libp2p-pubsub -================== +## Table of contents -[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) -[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://libp2p.io/) -[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) -[![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) -[![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-pubsub/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-pubsub?branch=master) -[![Travis CI](https://travis-ci.org/libp2p/js-libp2p-pubsub.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-pubsub) -[![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-pubsub.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-pubsub) -[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-pubsub.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-pubsub) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) -[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -[![](https://img.shields.io/badge/pm-waffle-yellow.svg?style=flat-square)](https://waffle.io/libp2p/js-libp2p-pubsub) - -> libp2p-pubsub is the base protocol for libp2p pubsub implementations. This module is responsible for registering the protocol with libp2p, as well as managing the logic regarding pubsub connections with other peers. - -## Lead Maintainer - -[Vasco Santos](https://github.com/vasco-santos). - -## Table of Contents - -- [Install](#install) - [Usage](#usage) -- [API](#api) -- [Contribute](#contribute) - [License](#license) - -## Install - -```sh -> npm install libp2p-pubsub -``` + - [Contribution](#contribution) ## Usage -`libp2p-pubsub` abstracts the implementation protocol registration within `libp2p` and takes care of all the protocol connections. This way, a pubsub implementation can focus on its routing algorithm, instead of also needing to create the setup for it. - -A pubsub implementation **MUST** override the `_processMessages`, `publish`, `subscribe`, `unsubscribe` and `getTopics` functions. - -Other functions, such as `_onPeerConnected`, `_onPeerDisconnected`, `_addPeer`, `_removePeer`, `start` and `stop` may be overwritten if the pubsub implementation needs to customize their logic. Implementations overriding `start` and `stop` **MUST** call `super`. The `start` function is responsible for registering the pubsub protocol with libp2p, while the `stop` function is responsible for unregistering the pubsub protocol and closing pubsub connections. - -All the remaining functions **MUST NOT** be overwritten. - -The following example aims to show how to create your pubsub implementation extending this base protocol. The pubsub implementation will handle the subscriptions logic. - -TODO: add explanation for registrar! - -```JavaScript -const Pubsub = require('libp2p-pubsub') - -class PubsubImplementation extends Pubsub { - constructor({ peerId, registrar, ...options }) - super({ - debugName: 'libp2p:pubsub', - multicodecs: '/pubsub-implementation/1.0.0', - peerId: peerId, - registrar: registrar, - signMessages: options.signMessages, - strictSigning: options.strictSigning - }) - } - - _processMessages(idB58Str, conn, peer) { - // Required to be implemented by the subclass - // Process each message accordingly - } - - publish() { - // Required to be implemented by the subclass - } - - subscribe() { - // Required to be implemented by the subclass - } +```console +npm i libp2p-pubsub +``` - unsubscribe() { - // Required to be implemented by the subclass - } +```javascript +import { PubSubBaseProtocol } from '@libp2p/pubsub' - getTopics() { - // Required to be implemented by the subclass - } +class MyPubsubImplementation extends PubSubBaseProtocol { + // .. extra methods here } ``` -## API - -The following specified API should be the base API for a pubsub implementation on top of `libp2p`. - -### Start - -Starts the pubsub subsystem. The protocol will be registered to `libp2p`, which will result in pubsub being notified when peers who support the protocol connect/disconnect to `libp2p`. - -#### `pubsub.start()` - -##### Returns - -| Type | Description | -|------|-------------| -| `Promise` | resolves once pubsub starts | - -### Stop - -Stops the pubsub subsystem. The protocol will be unregistered from `libp2p`, which will remove all listeners for the protocol and the established connections will be closed. - -#### `pubsub.stop()` - -##### Returns - -| Type | Description | -|------|-------------| -| `Promise` | resolves once pubsub stops | - -### Publish - -Publish data messages to pubsub topics. - -#### `pubsub.publish(topics, messages)` - -##### Parameters - -| Name | Type | Description | -|------|------|-------------| -| topics | `Array|string` | set of pubsub topics | -| messages | `Array|any` | set of messages to publish | - -##### Returns - -| Type | Description | -|------|-------------| -| `Promise` | resolves once messages are published to the network | - -### Subscribe - -Subscribe to the given topic(s). - -#### `pubsub.subscribe(topics)` - -##### Parameters - -| Name | Type | Description | -|------|------|-------------| -| topics | `Array|string` | set of pubsub topics | - -### Unsubscribe - -Unsubscribe from the given topic(s). - -#### `pubsub.unsubscribe(topics)` - -##### Parameters - -| Name | Type | Description | -|------|------|-------------| -| topics | `Array|string` | set of pubsub topics | - -### Get Topics - -Get the list of topics which the peer is subscribed to. - -#### `pubsub.getTopics()` - -##### Returns - -| Type | Description | -|------|-------------| -| `Array` | Array of subscribed topics | - -### Get Peers Subscribed to a topic - -Get a list of the [PeerId](https://github.com/libp2p/js-peer-id) strings that are subscribed to one topic. - -#### `pubsub.getSubscribers(topic)` - -##### Parameters - -| Name | Type | Description | -|------|------|-------------| -| topic | `string` | pubsub topic | - -##### Returns - -| Type | Description | -|------|-------------| -| `Array` | Array of base-58 PeerId's | - -### Validate - -Validates the signature of a message. - -#### `pubsub.validate(message)` - -##### Parameters - -| Name | Type | Description | -|------|------|-------------| -| message | `Message` | a pubsub message | - -#### Returns - -| Type | Description | -|------|-------------| -| `Promise` | resolves to true if the message is valid | - -## Implementations using this base protocol - -You can use the following implementations as examples for building your own pubsub implementation. - -- [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub) -- [ChainSafe/js-libp2p-gossipsub](https://github.com/ChainSafe/js-libp2p-gossipsub) - -## Contribute +## License -Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-pubsub/issues)! +Licensed under either of -This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + * Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0) + * MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT) -## License +### Contribution -Copyright (c) Protocol Labs, Inc. under the **MIT License**. See [LICENSE file](./LICENSE) for details. +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/package.json b/package.json index 1e36bf2d30..a2bde0036a 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,204 @@ { - "name": "libp2p-pubsub", - "version": "0.6.0", - "description": "Pubsub base protocol for libp2p pubsub routers", - "leadMaintainer": "Vasco Santos ", - "main": "src/index.js", - "scripts": { - "lint": "aegir lint", - "test": "aegir test", - "test:node": "aegir test -t node", - "test:browser": "aegir test -t browser", - "build": "aegir build", - "docs": "aegir-docs", - "release": "aegir release --target node --docs", - "release-minor": "aegir release --type minor --docs", - "release-major": "aegir release --type major --docs", - "coverage": "aegir coverage", - "coverage-publish": "aegir coverage --provider coveralls" - }, - "files": [ - "src", - "dist" - ], - "pre-push": [ - "lint" - ], + "name": "@libp2p/pubsub", + "version": "1.3.0", + "description": "libp2p pubsub base class", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-pubsub#readme", "repository": { "type": "git", "url": "git+https://github.com/libp2p/js-libp2p-pubsub.git" }, - "keywords": [ - "IPFS", - "libp2p", - "pubsub", - "gossip", - "flood", - "flooding" - ], - "license": "MIT", "bugs": { "url": "https://github.com/libp2p/js-libp2p-pubsub/issues" }, - "homepage": "https://github.com/libp2p/js-libp2p-pubsub#readme", - "devDependencies": { - "aegir": "^25.0.0", - "benchmark": "^2.1.4", - "chai": "^4.2.0", - "chai-spies": "^1.0.0", - "dirty-chai": "^2.0.1", - "it-pair": "^1.0.0", - "multiaddr": "^8.0.0", - "sinon": "^9.0.0" + "keywords": [ + "interface", + "libp2p" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "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/src", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "./errors": { + "import": "./dist/src/errors.js", + "types": "./dist/src/errors.d.ts" + }, + "./peer-streams": { + "import": "./dist/src/peer-streams.js", + "types": "./dist/src/peer-streams.d.ts" + }, + "./signature-policy": { + "import": "./dist/src/signature-policy.js", + "types": "./dist/src/signature-policy.d.ts" + }, + "./utils": { + "import": "./dist/src/utils.js", + "types": "./dist/src/utils.d.ts" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "test/message/*.d.ts", + "test/message/*.js" + ] + }, + "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": "chore", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "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": "Trivial Changes" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "generate": "protons test/message/rpc.proto", + "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", + "release": "aegir release" }, "dependencies": { - "debug": "^4.1.1", - "err-code": "^2.0.0", - "it-length-prefixed": "^3.0.0", - "it-pipe": "^1.0.1", - "it-pushable": "^1.3.2", - "libp2p-crypto": "^0.18.0", - "libp2p-interfaces": "^0.4.0", - "multibase": "^3.0.0", - "peer-id": "^0.14.0", - "protons": "^2.0.0", - "uint8arrays": "^1.1.0" + "@libp2p/crypto": "^0.22.8", + "@libp2p/interfaces": "^2.0.0", + "@libp2p/logger": "^1.1.0", + "@libp2p/peer-collections": "^1.0.0", + "@libp2p/peer-id": "^1.1.0", + "@libp2p/topology": "^1.1.0", + "@multiformats/multiaddr": "^10.1.5", + "abortable-iterator": "^4.0.2", + "err-code": "^3.0.1", + "iso-random-stream": "^2.0.0", + "it-length-prefixed": "^7.0.1", + "it-pipe": "^2.0.3", + "it-pushable": "^2.0.1", + "multiformats": "^9.6.3", + "p-queue": "^7.2.0", + "uint8arrays": "^3.0.0" }, - "contributors": [ - "Vasco Santos ", - "Jacob Heun ", - "Mikerah ", - "Cayman ", - "Topper Bowers ", - "Alex Potsides ", - "Hugo Dias ", - "Alan Shaw " - ] + "devDependencies": { + "@libp2p/peer-id-factory": "^1.0.0", + "aegir": "^37.0.7", + "delay": "^5.0.0", + "it-pair": "^2.0.2", + "p-defer": "^4.0.0", + "p-wait-for": "^4.1.0", + "protons": "^3.0.4", + "protons-runtime": "^1.0.4", + "sinon": "^14.0.0", + "util": "^0.12.4" + } } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000000..cbdeb51da5 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,53 @@ + +export const codes = { + /** + * Signature policy is invalid + */ + ERR_INVALID_SIGNATURE_POLICY: 'ERR_INVALID_SIGNATURE_POLICY', + /** + * Signature policy is unhandled + */ + ERR_UNHANDLED_SIGNATURE_POLICY: 'ERR_UNHANDLED_SIGNATURE_POLICY', + + // Strict signing codes + + /** + * Message expected to have a `signature`, but doesn't + */ + ERR_MISSING_SIGNATURE: 'ERR_MISSING_SIGNATURE', + /** + * Message expected to have a `seqno`, but doesn't + */ + ERR_MISSING_SEQNO: 'ERR_MISSING_SEQNO', + /** + * Message expected to have a `key`, but doesn't + */ + ERR_MISSING_KEY: 'ERR_MISSING_KEY', + /** + * Message `signature` is invalid + */ + ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE', + /** + * Message expected to have a `from`, but doesn't + */ + ERR_MISSING_FROM: 'ERR_MISSING_FROM', + + // Strict no-signing codes + + /** + * Message expected to not have a `from`, but does + */ + ERR_UNEXPECTED_FROM: 'ERR_UNEXPECTED_FROM', + /** + * Message expected to not have a `signature`, but does + */ + ERR_UNEXPECTED_SIGNATURE: 'ERR_UNEXPECTED_SIGNATURE', + /** + * Message expected to not have a `key`, but does + */ + ERR_UNEXPECTED_KEY: 'ERR_UNEXPECTED_KEY', + /** + * Message expected to not have a `seqno`, but does + */ + ERR_UNEXPECTED_SEQNO: 'ERR_UNEXPECTED_SEQNO' +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 96741cb2b7..0000000000 --- a/src/index.js +++ /dev/null @@ -1,374 +0,0 @@ -'use strict' - -const debug = require('debug') -const EventEmitter = require('events') -const errcode = require('err-code') - -const PeerId = require('peer-id') -const MulticodecTopology = require('libp2p-interfaces/src/topology/multicodec-topology') - -const message = require('./message') -const Peer = require('./peer') -const utils = require('./utils') -const { - signMessage, - verifySignature -} = require('./message/sign') - -function validateRegistrar (registrar) { - // registrar handling - if (typeof registrar !== 'object') { - throw new Error('a registrar object is required') - } - - if (typeof registrar.handle !== 'function') { - throw new Error('a handle function must be provided in registrar') - } - - if (typeof registrar.register !== 'function') { - throw new Error('a register function must be provided in registrar') - } - - if (typeof registrar.unregister !== 'function') { - throw new Error('a unregister function must be provided in registrar') - } -} - -/** - * PubsubBaseProtocol handles the peers and connections logic for pubsub routers - */ -class PubsubBaseProtocol extends EventEmitter { - /** - * @param {Object} props - * @param {String} props.debugName log namespace - * @param {Array|string} props.multicodecs protocol identificers to connect - * @param {PeerId} props.peerId peer's peerId - * @param {Object} props.registrar registrar for libp2p protocols - * @param {function} props.registrar.handle - * @param {function} props.registrar.register - * @param {function} props.registrar.unregister - * @param {boolean} [props.signMessages] if messages should be signed, defaults to true - * @param {boolean} [props.strictSigning] if message signing should be required, defaults to true - * @abstract - */ - constructor ({ - debugName, - multicodecs, - peerId, - registrar, - signMessages = true, - strictSigning = true - }) { - if (typeof debugName !== 'string') { - throw new Error('a debugname `string` is required') - } - - if (!multicodecs) { - throw new Error('multicodecs are required') - } - - if (!PeerId.isPeerId(peerId)) { - throw new Error('peerId must be an instance of `peer-id`') - } - - validateRegistrar(registrar) - - super() - - this.log = debug(debugName) - this.log.err = debug(`${debugName}:error`) - - this.multicodecs = utils.ensureArray(multicodecs) - this.registrar = registrar - - this.started = false - - this.peerId = peerId - - /** - * Map of topics to which peers are subscribed to - * - * @type {Map} - */ - this.topics = new Map() - - /** - * Map of peers. - * - * @type {Map} - */ - this.peers = new Map() - - // Message signing - this.signMessages = signMessages - - /** - * If message signing should be required for incoming messages - * @type {boolean} - */ - this.strictSigning = strictSigning - - this._registrarId = undefined - this._onIncomingStream = this._onIncomingStream.bind(this) - this._onPeerConnected = this._onPeerConnected.bind(this) - this._onPeerDisconnected = this._onPeerDisconnected.bind(this) - } - - /** - * Register the pubsub protocol onto the libp2p node. - * @returns {Promise} - */ - async start () { - if (this.started) { - return - } - this.log('starting') - - // Incoming streams - this.registrar.handle(this.multicodecs, this._onIncomingStream) - - // register protocol with topology - const topology = new MulticodecTopology({ - multicodecs: this.multicodecs, - handlers: { - onConnect: this._onPeerConnected, - onDisconnect: this._onPeerDisconnected - } - }) - this._registrarId = await this.registrar.register(topology) - - this.log('started') - this.started = true - } - - /** - * Unregister the pubsub protocol and the streams with other peers will be closed. - * @returns {Promise} - */ - async stop () { - if (!this.started) { - return - } - - // unregister protocol and handlers - await this.registrar.unregister(this._registrarId) - - this.log('stopping') - this.peers.forEach((peer) => peer.close()) - - this.peers = new Map() - this.started = false - this.log('stopped') - } - - /** - * On an incoming stream event. - * @private - * @param {Object} props - * @param {string} props.protocol - * @param {DuplexStream} props.strean - * @param {Connection} props.connection connection - */ - _onIncomingStream ({ protocol, stream, connection }) { - const peerId = connection.remotePeer - const idB58Str = peerId.toB58String() - const peer = this._addPeer(peerId, [protocol]) - - this._processMessages(idB58Str, stream, peer) - } - - /** - * Registrar notifies a connection successfully with pubsub protocol. - * @private - * @param {PeerId} peerId remote peer-id - * @param {Connection} conn connection to the peer - */ - async _onPeerConnected (peerId, conn) { - const idB58Str = peerId.toB58String() - this.log('connected', idB58Str) - - const peer = this._addPeer(peerId, this.multicodecs) - - try { - const { stream } = await conn.newStream(this.multicodecs) - peer.attachConnection(stream) - } catch (err) { - this.log.err(err) - } - } - - /** - * Registrar notifies a closing connection with pubsub protocol. - * @private - * @param {PeerId} peerId peerId - * @param {Error} err error for connection end - */ - _onPeerDisconnected (peerId, err) { - const idB58Str = peerId.toB58String() - const peer = this.peers.get(idB58Str) - - this.log('connection ended', idB58Str, err ? err.message : '') - this._removePeer(peer) - } - - /** - * Add a new connected peer to the peers map. - * @private - * @param {PeerId} peerId - * @param {Array} protocols - * @returns {Peer} - */ - _addPeer (peerId, protocols) { - const id = peerId.toB58String() - let existing = this.peers.get(id) - - if (!existing) { - this.log('new peer', id) - - const peer = new Peer({ - id: peerId, - protocols - }) - - this.peers.set(id, peer) - existing = peer - - peer.once('close', () => this._removePeer(peer)) - } - - return existing - } - - /** - * Remove a peer from the peers map. - * @private - * @param {Peer} peer peer state - * @returns {Peer} - */ - _removePeer (peer) { - if (!peer) return - const id = peer.id.toB58String() - - this.log('delete peer', id) - this.peers.delete(id) - - return peer - } - - /** - * Validates the given message. The signature will be checked for authenticity. - * @param {rpc.RPC.Message} message - * @returns {Promise} - */ - async validate (message) { // eslint-disable-line require-await - // If strict signing is on and we have no signature, abort - if (this.strictSigning && !message.signature) { - this.log('Signing required and no signature was present, dropping message:', message) - return false - } - - // Check the message signature if present - if (message.signature) { - return verifySignature(message) - } else { - return true - } - } - - /** - * Normalizes the message and signs it, if signing is enabled - * @private - * @param {Message} message - * @returns {Promise} - */ - _buildMessage (message) { - const msg = utils.normalizeOutRpcMessage(message) - if (this.signMessages) { - return signMessage(this.peerId, msg) - } else { - return message - } - } - - /** - * Get a list of the peer-ids that are subscribed to one topic. - * @param {string} topic - * @returns {Array} - */ - getSubscribers (topic) { - if (!this.started) { - throw errcode(new Error('not started yet'), 'ERR_NOT_STARTED_YET') - } - - if (!topic || typeof topic !== 'string') { - throw errcode(new Error('a string topic must be provided'), 'ERR_NOT_VALID_TOPIC') - } - - return Array.from(this.peers.values()) - .filter((peer) => peer.topics.has(topic)) - .map((peer) => peer.id.toB58String()) - } - - /** - * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation. - * For example, a Floodsub implementation might simply publish each message to each topic for every peer - * @abstract - * @param {Array|string} topics - * @param {Array|any} messages - * @returns {Promise} - * - */ - publish (topics, messages) { - throw errcode(new Error('publish must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') - } - - /** - * Overriding the implementation of subscribe should handle the appropriate algorithms for the publish/subscriber implementation. - * For example, a Floodsub implementation might simply send a message for every peer showing interest in the topics - * @abstract - * @param {Array|string} topics - * @returns {void} - */ - subscribe (topics) { - throw errcode(new Error('subscribe must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') - } - - /** - * Overriding the implementation of unsubscribe should handle the appropriate algorithms for the publish/subscriber implementation. - * For example, a Floodsub implementation might simply send a message for every peer revoking interest in the topics - * @abstract - * @param {Array|string} topics - * @returns {void} - */ - unsubscribe (topics) { - throw errcode(new Error('unsubscribe must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') - } - - /** - * Overriding the implementation of getTopics should handle the appropriate algorithms for the publish/subscriber implementation. - * Get the list of subscriptions the peer is subscribed to. - * @abstract - * @returns {Array} - */ - getTopics () { - throw errcode(new Error('getTopics must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') - } - - /** - * Overriding the implementation of _processMessages should keep the connection and is - * responsible for processing each RPC message received by other peers. - * @abstract - * @param {string} idB58Str peer id string in base58 - * @param {Connection} conn connection - * @param {Peer} peer A Pubsub Peer - * @returns {void} - * - */ - _processMessages (idB58Str, conn, peer) { - throw errcode(new Error('_processMessages must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') - } -} - -module.exports = PubsubBaseProtocol -module.exports.message = message -module.exports.utils = utils diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..06d64dcd6b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,686 @@ +import { logger } from '@libp2p/logger' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import errcode from 'err-code' +import { pipe } from 'it-pipe' +import Queue from 'p-queue' +import { createTopology } from '@libp2p/topology' +import { codes } from './errors.js' +import { PeerStreams as PeerStreamsImpl } from './peer-streams.js' +import { toMessage, ensureArray, randomSeqno, noSignMsgId, msgId, toRpcMessage } from './utils.js' +import { + signMessage, + verifySignature +} from './sign.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { IncomingStreamData } from '@libp2p/interfaces/registrar' +import type { Connection } from '@libp2p/interfaces/connection' +import type { PubSub, Message, StrictNoSign, StrictSign, PubSubInit, PubSubEvents, PeerStreams, PubSubRPCMessage, PubSubRPC, PubSubRPCSubscription, SubscriptionChangeData, PublishResult } from '@libp2p/interfaces/pubsub' +import { PeerMap, PeerSet } from '@libp2p/peer-collections' +import { Components, Initializable } from '@libp2p/interfaces/components' + +const log = logger('libp2p:pubsub') + +export interface TopicValidator { (topic: string, message: Message): Promise } + +/** + * PubSubBaseProtocol handles the peers and connections logic for pubsub routers + * and specifies the API that pubsub routers should have. + */ +export abstract class PubSubBaseProtocol extends EventEmitter implements PubSub, Initializable { + public started: boolean + /** + * Map of topics to which peers are subscribed to + */ + public topics: Map + /** + * List of our subscriptions + */ + public subscriptions: Set + /** + * Map of peer streams + */ + public peers: PeerMap + /** + * The signature policy to follow by default + */ + public globalSignaturePolicy: typeof StrictNoSign | typeof StrictSign + /** + * If router can relay received messages, even if not subscribed + */ + public canRelayMessage: boolean + /** + * if publish should emit to self, if subscribed + */ + public emitSelf: boolean + /** + * Topic validator map + * + * Keyed by topic + * Topic validators are functions with the following input: + */ + public topicValidators: Map + public queue: Queue + public multicodecs: string[] + public components: Components = new Components() + + private _registrarTopologyId: string | undefined + protected enabled: boolean + + constructor (props: PubSubInit) { + super() + + const { + multicodecs = [], + globalSignaturePolicy = 'StrictSign', + canRelayMessage = false, + emitSelf = false, + messageProcessingConcurrency = 10 + } = props + + this.multicodecs = ensureArray(multicodecs) + this.enabled = props.enabled !== false + this.started = false + this.topics = new Map() + this.subscriptions = new Set() + this.peers = new PeerMap() + this.globalSignaturePolicy = globalSignaturePolicy === 'StrictNoSign' ? 'StrictNoSign' : 'StrictSign' + this.canRelayMessage = canRelayMessage + this.emitSelf = emitSelf + this.topicValidators = new Map() + this.queue = new Queue({ concurrency: messageProcessingConcurrency }) + + this._onIncomingStream = this._onIncomingStream.bind(this) + this._onPeerConnected = this._onPeerConnected.bind(this) + this._onPeerDisconnected = this._onPeerDisconnected.bind(this) + } + + init (components: Components) { + this.components = components + } + + // LIFECYCLE METHODS + + /** + * Register the pubsub protocol onto the libp2p node. + * + * @returns {void} + */ + async start () { + if (this.started || !this.enabled) { + return + } + + log('starting') + + // Incoming streams + // Called after a peer dials us + await this.components.getRegistrar().handle(this.multicodecs, this._onIncomingStream) + + // register protocol with topology + // Topology callbacks called on connection manager changes + const topology = createTopology({ + onConnect: this._onPeerConnected, + onDisconnect: this._onPeerDisconnected + }) + this._registrarTopologyId = await this.components.getRegistrar().register(this.multicodecs, topology) + + log('started') + this.started = true + } + + /** + * Unregister the pubsub protocol and the streams with other peers will be closed. + */ + async stop () { + if (!this.started || !this.enabled) { + return + } + + // unregister protocol and handlers + if (this._registrarTopologyId != null) { + this.components.getRegistrar().unregister(this._registrarTopologyId) + } + + await this.components.getRegistrar().unhandle(this.multicodecs) + + log('stopping') + for (const peerStreams of this.peers.values()) { + peerStreams.close() + } + + this.peers.clear() + this.subscriptions = new Set() + this.started = false + log('stopped') + } + + isStarted () { + return this.started + } + + /** + * On an inbound stream opened + */ + protected _onIncomingStream (data: IncomingStreamData) { + const { protocol, stream, connection } = data + const peerId = connection.remotePeer + const peer = this.addPeer(peerId, protocol) + const inboundStream = peer.attachInboundStream(stream) + + this.processMessages(peerId, inboundStream, peer) + .catch(err => log(err)) + } + + /** + * Registrar notifies an established connection with pubsub protocol + */ + protected _onPeerConnected (peerId: PeerId, conn: Connection) { + log('connected %p', peerId) + + void Promise.resolve().then(async () => { + try { + const { stream, protocol } = await conn.newStream(this.multicodecs) + const peer = this.addPeer(peerId, protocol) + await peer.attachOutboundStream(stream) + } catch (err: any) { + log.error(err) + } + + // Immediately send my own subscriptions to the newly established conn + this.send(peerId, { subscriptions: Array.from(this.subscriptions).map(sub => sub.toString()), subscribe: true }) + }) + .catch(err => { + log.error(err) + }) + } + + /** + * Registrar notifies a closing connection with pubsub protocol + */ + protected _onPeerDisconnected (peerId: PeerId, conn?: Connection) { + const idB58Str = peerId.toString() + + log('connection ended', idB58Str) + this._removePeer(peerId) + } + + /** + * Notifies the router that a peer has been connected + */ + addPeer (peerId: PeerId, protocol: string): PeerStreams { + const existing = this.peers.get(peerId) + + // If peer streams already exists, do nothing + if (existing != null) { + return existing + } + + // else create a new peer streams + log('new peer %p', peerId) + + const peerStreams: PeerStreams = new PeerStreamsImpl({ + id: peerId, + protocol + }) + + this.peers.set(peerId, peerStreams) + peerStreams.addEventListener('close', () => this._removePeer(peerId), { + once: true + }) + + return peerStreams + } + + /** + * Notifies the router that a peer has been disconnected + */ + protected _removePeer (peerId: PeerId) { + const peerStreams = this.peers.get(peerId) + if (peerStreams == null) { + return + } + + // close peer streams + peerStreams.close() + + // delete peer streams + log('delete peer %p', peerId) + this.peers.delete(peerId) + + // remove peer from topics map + for (const peers of this.topics.values()) { + peers.delete(peerId) + } + + return peerStreams + } + + // MESSAGE METHODS + + /** + * Responsible for processing each RPC message received by other peers. + */ + async processMessages (peerId: PeerId, stream: AsyncIterable, peerStreams: PeerStreams) { + try { + await pipe( + stream, + async (source) => { + for await (const data of source) { + const rpcMsg = this.decodeRpc(data) + const messages: PubSubRPCMessage[] = [] + + for (const msg of (rpcMsg.messages ?? [])) { + if (msg.from == null || msg.data == null || msg.topic == null) { + log('message from %p was missing from, data or topic fields, dropping', peerId) + continue + } + + messages.push({ + from: msg.from, + data: msg.data, + topic: msg.topic, + sequenceNumber: msg.sequenceNumber ?? undefined, + signature: msg.signature ?? undefined, + key: msg.key ?? undefined + }) + } + + // Since processRpc may be overridden entirely in unsafe ways, + // the simplest/safest option here is to wrap in a function and capture all errors + // to prevent a top-level unhandled exception + // This processing of rpc messages should happen without awaiting full validation/execution of prior messages + this.processRpc(peerId, peerStreams, { + subscriptions: (rpcMsg.subscriptions ?? []).map(sub => ({ + subscribe: Boolean(sub.subscribe), + topic: sub.topic ?? '' + })), + messages + }) + .catch(err => log(err)) + } + } + ) + } catch (err: any) { + this._onPeerDisconnected(peerStreams.id, err) + } + } + + /** + * Handles an rpc request from a peer + */ + async processRpc (from: PeerId, peerStreams: PeerStreams, rpc: PubSubRPC): Promise { + if (!this.acceptFrom(from)) { + log('received message from unacceptable peer %p', from) + return false + } + + log('rpc from %p', from) + + const { subscriptions, messages } = rpc + + if (subscriptions != null && subscriptions.length > 0) { + log('subscription update from %p', from) + + // update peer subscriptions + subscriptions.forEach((subOpt) => { + this.processRpcSubOpt(from, subOpt) + }) + + super.dispatchEvent(new CustomEvent('subscription-change', { + detail: { + peerId: peerStreams.id, + subscriptions: subscriptions.map(({ topic, subscribe }) => ({ + topic: `${topic ?? ''}`, + subscribe: Boolean(subscribe) + })) + } + })) + } + + if (messages != null && messages.length > 0) { + log('messages from %p', from) + + this.queue.addAll(messages.map(message => async () => { + if (message.topic == null || (!this.subscriptions.has(message.topic) && !this.canRelayMessage)) { + log('received message we didn\'t subscribe to. Dropping.') + return false + } + + try { + const msg = await toMessage(message) + + await this.processMessage(from, msg) + } catch (err: any) { + log.error(err) + } + })) + .catch(err => log(err)) + } + + return true + } + + /** + * Handles a subscription change from a peer + */ + processRpcSubOpt (id: PeerId, subOpt: PubSubRPCSubscription) { + const t = subOpt.topic + + if (t == null) { + return + } + + let topicSet = this.topics.get(t) + if (topicSet == null) { + topicSet = new PeerSet() + this.topics.set(t, topicSet) + } + + if (subOpt.subscribe === true) { + // subscribe peer to new topic + topicSet.add(id) + } else { + // unsubscribe from existing topic + topicSet.delete(id) + } + } + + /** + * Handles a message from a peer + */ + async processMessage (from: PeerId, msg: Message) { + if (this.components.getPeerId().equals(from) && !this.emitSelf) { + return + } + + // Ensure the message is valid before processing it + try { + await this.validate(msg) + } catch (err: any) { + log('Message is invalid, dropping it. %O', err) + return + } + + if (this.subscriptions.has(msg.topic)) { + const isFromSelf = this.components.getPeerId().equals(from) + + if (!isFromSelf || this.emitSelf) { + super.dispatchEvent(new CustomEvent('message', { + detail: msg + })) + } + } + + await this.publishMessage(from, msg) + } + + /** + * The default msgID implementation + * Child class can override this. + */ + getMsgId (msg: Message) { + const signaturePolicy = this.globalSignaturePolicy + switch (signaturePolicy) { + case 'StrictSign': + if (msg.sequenceNumber == null) { + throw errcode(new Error('Need seqno when signature policy is StrictSign but it was missing'), codes.ERR_MISSING_SEQNO) + } + + if (msg.key == null) { + throw errcode(new Error('Need key when signature policy is StrictSign but it was missing'), codes.ERR_MISSING_KEY) + } + + return msgId(msg.key, msg.sequenceNumber) + case 'StrictNoSign': + return noSignMsgId(msg.data) + default: + throw errcode(new Error('Cannot get message id: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY) + } + } + + /** + * Whether to accept a message from a peer + * Override to create a graylist + */ + acceptFrom (id: PeerId) { + return true + } + + /** + * Decode Uint8Array into an RPC object. + * This can be override to use a custom router protobuf. + */ + abstract decodeRpc (bytes: Uint8Array): PubSubRPC + + /** + * Encode RPC object into a Uint8Array. + * This can be override to use a custom router protobuf. + */ + abstract encodeRpc (rpc: PubSubRPC): Uint8Array + + /** + * Encode RPC object into a Uint8Array. + * This can be override to use a custom router protobuf. + */ + abstract encodeMessage (rpc: PubSubRPCMessage): Uint8Array + + /** + * Send an rpc object to a peer + */ + send (peer: PeerId, data: { messages?: Message[], subscriptions?: string[], subscribe?: boolean }) { + const { messages, subscriptions, subscribe } = data + + return this.sendRpc(peer, { + subscriptions: (subscriptions ?? []).map(str => ({ topic: str, subscribe: Boolean(subscribe) })), + messages: (messages ?? []).map(toRpcMessage) + }) + } + + /** + * Send an rpc object to a peer + */ + sendRpc (peer: PeerId, rpc: PubSubRPC) { + const peerStreams = this.peers.get(peer) + + if (peerStreams == null || !peerStreams.isWritable) { + log.error('Cannot send RPC to %p as there is no open stream to it available', peer) + + return + } + + peerStreams.write(this.encodeRpc(rpc)) + } + + /** + * Validates the given message. The signature will be checked for authenticity. + * Throws an error on invalid messages + */ + async validate (message: Message) { // eslint-disable-line require-await + const signaturePolicy = this.globalSignaturePolicy + switch (signaturePolicy) { + case 'StrictNoSign': + if (message.signature != null) { + throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE) + } + if (message.key != null) { + throw errcode(new Error('StrictNoSigning: key should not be present'), codes.ERR_UNEXPECTED_KEY) + } + if (message.sequenceNumber != null) { + throw errcode(new Error('StrictNoSigning: seqno should not be present'), codes.ERR_UNEXPECTED_SEQNO) + } + break + case 'StrictSign': + if (message.signature == null) { + throw errcode(new Error('StrictSigning: Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE) + } + if (message.sequenceNumber == null) { + throw errcode(new Error('StrictSigning: Signing required and no seqno was present'), codes.ERR_MISSING_SEQNO) + } + if (!(await verifySignature(message, this.encodeMessage.bind(this)))) { + throw errcode(new Error('StrictSigning: Invalid message signature'), codes.ERR_INVALID_SIGNATURE) + } + break + default: + throw errcode(new Error('Cannot validate message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY) + } + + const validatorFn = this.topicValidators.get(message.topic) + + if (validatorFn != null) { + await validatorFn(message.topic, message) + } + } + + /** + * Normalizes the message and signs it, if signing is enabled. + * Should be used by the routers to create the message to send. + */ + async buildMessage (message: Message) { + const signaturePolicy = this.globalSignaturePolicy + switch (signaturePolicy) { + case 'StrictSign': + message.sequenceNumber = randomSeqno() + return await signMessage(this.components.getPeerId(), message, this.encodeMessage.bind(this)) + case 'StrictNoSign': + return await Promise.resolve(message) + default: + throw errcode(new Error('Cannot build message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY) + } + } + + // API METHODS + + /** + * Get a list of the peer-ids that are subscribed to one topic. + */ + getSubscribers (topic: string) { + if (!this.started) { + throw errcode(new Error('not started yet'), 'ERR_NOT_STARTED_YET') + } + + if (topic == null) { + throw errcode(new Error('topic is required'), 'ERR_NOT_VALID_TOPIC') + } + + const peersInTopic = this.topics.get(topic.toString()) + + if (peersInTopic == null) { + return [] + } + + return Array.from(peersInTopic.values()) + } + + /** + * Publishes messages to all subscribed peers + */ + async publish (topic: string, data?: Uint8Array): Promise { + if (!this.started) { + throw new Error('Pubsub has not started') + } + + const message: Message = { + from: this.components.getPeerId(), + topic, + data: data ?? new Uint8Array(0) + } + + log('publish topic: %s from: %p data: %m', topic, message.from, message.data) + + const rpcMessage = await this.buildMessage(message) + let emittedToSelf = false + + // dispatch the event if we are interested + if (this.emitSelf) { + if (this.subscriptions.has(topic)) { + emittedToSelf = true + super.dispatchEvent(new CustomEvent('message', { + detail: rpcMessage + })) + + if (this.listenerCount(topic) === 0) { + this.unsubscribe(topic) + } + } + } + + // send to all the other peers + const result = await this.publishMessage(this.components.getPeerId(), rpcMessage) + + if (emittedToSelf) { + result.recipients = [...result.recipients, this.components.getPeerId()] + } + + return result + } + + /** + * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation. + * For example, a Floodsub implementation might simply publish each message to each topic for every peer. + * + * `sender` might be this peer, or we might be forwarding a message on behalf of another peer, in which case sender + * is the peer we received the message from, which may not be the peer the message was created by. + */ + abstract publishMessage (sender: PeerId, message: Message): Promise + + /** + * Subscribes to a given topic. + */ + subscribe (topic: string) { + if (!this.started) { + throw new Error('Pubsub has not started') + } + + if (!this.subscriptions.has(topic)) { + this.subscriptions.add(topic) + + for (const peerId of this.peers.keys()) { + this.send(peerId, { subscriptions: [topic], subscribe: true }) + } + } + } + + /** + * Unsubscribe from the given topic + */ + unsubscribe (topic: string) { + if (!this.started) { + throw new Error('Pubsub is not started') + } + + // @ts-expect-error topic should be a key of the event map + super.removeEventListener(topic) + + const wasSubscribed = this.subscriptions.has(topic) + const listeners = this.listenerCount(topic) + + log('unsubscribe from %s - am subscribed %s, listeners %d', topic, wasSubscribed, listeners) + + if (wasSubscribed && listeners === 0) { + this.subscriptions.delete(topic) + + for (const peerId of this.peers.keys()) { + this.send(peerId, { subscriptions: [topic], subscribe: false }) + } + } + } + + /** + * Get the list of topics which the peer is subscribed to. + */ + getTopics () { + if (!this.started) { + throw new Error('Pubsub is not started') + } + + return Array.from(this.subscriptions) + } + + getPeers () { + if (!this.started) { + throw new Error('Pubsub is not started') + } + + return Array.from(this.peers.keys()) + } +} diff --git a/src/message/index.js b/src/message/index.js deleted file mode 100644 index 320ab4cf7d..0000000000 --- a/src/message/index.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -const protons = require('protons') - -const rpcProto = protons(require('./rpc.proto.js')) -const RPC = rpcProto.RPC -const topicDescriptorProto = protons(require('./topic-descriptor.proto.js')) - -exports = module.exports -exports.rpc = rpcProto -exports.td = topicDescriptorProto -exports.RPC = RPC -exports.Message = RPC.Message -exports.SubOpts = RPC.SubOpts diff --git a/src/message/rpc.proto.js b/src/message/rpc.proto.js deleted file mode 100644 index 88b1f83427..0000000000 --- a/src/message/rpc.proto.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' -module.exports = ` -message RPC { - repeated SubOpts subscriptions = 1; - repeated Message msgs = 2; - - message SubOpts { - optional bool subscribe = 1; // subscribe or unsubcribe - optional string topicID = 2; - } - - message Message { - optional bytes from = 1; - optional bytes data = 2; - optional bytes seqno = 3; - repeated string topicIDs = 4; - optional bytes signature = 5; - optional bytes key = 6; - } -}` diff --git a/src/message/sign.js b/src/message/sign.js deleted file mode 100644 index ed9e064db6..0000000000 --- a/src/message/sign.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict' - -const PeerId = require('peer-id') -const { Message } = require('./index') -const uint8ArrayConcat = require('uint8arrays/concat') -const uint8ArrayFromString = require('uint8arrays/from-string') -const SignPrefix = uint8ArrayFromString('libp2p-pubsub:') - -/** - * Signs the provided message with the given `peerId` - * - * @param {PeerId} peerId - * @param {Message} message - * @returns {Promise} - */ -async function signMessage (peerId, message) { - // Get the message in bytes, and prepend with the pubsub prefix - const bytes = uint8ArrayConcat([ - SignPrefix, - Message.encode(message) - ]) - - const signature = await peerId.privKey.sign(bytes) - - return { - ...message, - signature: signature, - key: peerId.pubKey.bytes - } -} - -/** - * Verifies the signature of the given message - * @param {rpc.RPC.Message} message - * @returns {Promise} - */ -async function verifySignature (message) { - // Get message sans the signature - const baseMessage = { ...message } - delete baseMessage.signature - delete baseMessage.key - const bytes = uint8ArrayConcat([ - SignPrefix, - Message.encode(baseMessage) - ]) - - // Get the public key - const pubKey = await messagePublicKey(message) - - // verify the base message - return pubKey.verify(bytes, message.signature) -} - -/** - * Returns the PublicKey associated with the given message. - * If no, valid PublicKey can be retrieved an error will be returned. - * - * @param {Message} message - * @returns {Promise} - */ -async function messagePublicKey (message) { - if (message.key) { - const peerId = await PeerId.createFromPubKey(message.key) - - // the key belongs to the sender, return the key - if (peerId.isEqual(message.from)) return peerId.pubKey - // We couldn't validate pubkey is from the originator, error - throw new Error('Public Key does not match the originator') - } else { - // should be available in the from property of the message (peer id) - const from = PeerId.createFromBytes(message.from) - - if (from.pubKey) { - return from.pubKey - } else { - throw new Error('Could not get the public key from the originator id') - } - } -} - -module.exports = { - messagePublicKey, - signMessage, - SignPrefix, - verifySignature -} diff --git a/src/message/topic-descriptor.proto.js b/src/message/topic-descriptor.proto.js deleted file mode 100644 index 6e829ca579..0000000000 --- a/src/message/topic-descriptor.proto.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' -module.exports = ` -// topicCID = cid(merkledag_protobuf(topicDescriptor)); (not the topic.name) -message TopicDescriptor { - optional string name = 1; - optional AuthOpts auth = 2; - optional EncOpts enc = 2; - - message AuthOpts { - optional AuthMode mode = 1; - repeated bytes keys = 2; // root keys to trust - - enum AuthMode { - NONE = 0; // no authentication, anyone can publish - KEY = 1; // only messages signed by keys in the topic descriptor are accepted - WOT = 2; // web of trust, certificates can allow publisher set to grow - } - } - - message EncOpts { - optional EncMode mode = 1; - repeated bytes keyHashes = 2; // the hashes of the shared keys used (salted) - - enum EncMode { - NONE = 0; // no encryption, anyone can read - SHAREDKEY = 1; // messages are encrypted with shared key - WOT = 2; // web of trust, certificates can allow publisher set to grow - } - } -}` diff --git a/src/peer-streams.ts b/src/peer-streams.ts new file mode 100644 index 0000000000..0235a9deb9 --- /dev/null +++ b/src/peer-streams.ts @@ -0,0 +1,174 @@ +import { logger } from '@libp2p/logger' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import * as lp from 'it-length-prefixed' +import { pushable } from 'it-pushable' +import { pipe } from 'it-pipe' +import { abortableSource } from 'abortable-iterator' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { Stream } from '@libp2p/interfaces/connection' +import type { Pushable } from 'it-pushable' +import type { PeerStreamEvents } from '@libp2p/interfaces/pubsub' + +const log = logger('libp2p-pubsub:peer-streams') + +export interface PeerStreamsInit { + id: PeerId + protocol: string +} + +/** + * Thin wrapper around a peer's inbound / outbound pubsub streams + */ +export class PeerStreams extends EventEmitter { + public readonly id: PeerId + public readonly protocol: string + /** + * Write stream - it's preferable to use the write method + */ + public outboundStream?: Pushable + /** + * Read stream + */ + public inboundStream?: AsyncIterable + /** + * The raw outbound stream, as retrieved from conn.newStream + */ + private _rawOutboundStream?: Stream + /** + * The raw inbound stream, as retrieved from the callback from libp2p.handle + */ + private _rawInboundStream?: Stream + /** + * An AbortController for controlled shutdown of the inbound stream + */ + private readonly _inboundAbortController: AbortController + private closed: boolean + + constructor (init: PeerStreamsInit) { + super() + + this.id = init.id + this.protocol = init.protocol + + this._inboundAbortController = new AbortController() + this.closed = false + } + + /** + * Do we have a connection to read from? + */ + get isReadable () { + return Boolean(this.inboundStream) + } + + /** + * Do we have a connection to write on? + */ + get isWritable () { + return Boolean(this.outboundStream) + } + + /** + * Send a message to this peer. + * Throws if there is no `stream` to write to available. + */ + write (data: Uint8Array) { + if (this.outboundStream == null) { + const id = this.id.toString() + throw new Error('No writable connection to ' + id) + } + + this.outboundStream.push(data) + } + + /** + * Attach a raw inbound stream and setup a read stream + */ + attachInboundStream (stream: Stream) { + // Create and attach a new inbound stream + // The inbound stream is: + // - abortable, set to only return on abort, rather than throw + // - transformed with length-prefix transform + this._rawInboundStream = stream + this.inboundStream = abortableSource( + pipe( + this._rawInboundStream, + lp.decode() + ), + this._inboundAbortController.signal, + { returnOnAbort: true } + ) + + this.dispatchEvent(new CustomEvent('stream:inbound')) + return this.inboundStream + } + + /** + * Attach a raw outbound stream and setup a write stream + */ + async attachOutboundStream (stream: Stream) { + // If an outbound stream already exists, gently close it + const _prevStream = this.outboundStream + if (this.outboundStream != null) { + // End the stream without emitting a close event + await this.outboundStream.end() + } + + this._rawOutboundStream = stream + this.outboundStream = pushable({ + onEnd: (shouldEmit) => { + // close writable side of the stream + if (this._rawOutboundStream != null && this._rawOutboundStream.reset != null) { // eslint-disable-line @typescript-eslint/prefer-optional-chain + this._rawOutboundStream.reset() + } + + this._rawOutboundStream = undefined + this.outboundStream = undefined + if (shouldEmit != null) { + this.dispatchEvent(new CustomEvent('close')) + } + } + }) + + pipe( + this.outboundStream, + lp.encode(), + this._rawOutboundStream + ).catch((err: Error) => { + log.error(err) + }) + + // Only emit if the connection is new + if (_prevStream == null) { + this.dispatchEvent(new CustomEvent('stream:outbound')) + } + + return this.outboundStream + } + + /** + * Closes the open connection to peer + */ + close () { + if (this.closed) { + return + } + + this.closed = true + + // End the outbound stream + if (this.outboundStream != null) { + this.outboundStream.end() + } + // End the inbound stream + if (this.inboundStream != null) { + this._inboundAbortController.abort() + } + + this._rawOutboundStream = undefined + this.outboundStream = undefined + this._rawInboundStream = undefined + this.inboundStream = undefined + this.dispatchEvent(new CustomEvent('close')) + } +} diff --git a/src/peer.js b/src/peer.js deleted file mode 100644 index 5e966f310e..0000000000 --- a/src/peer.js +++ /dev/null @@ -1,202 +0,0 @@ -'use strict' - -const EventEmitter = require('events') - -const lp = require('it-length-prefixed') -const pushable = require('it-pushable') -const pipe = require('it-pipe') -const debug = require('debug') - -const log = debug('libp2p-pubsub:peer') -log.error = debug('libp2p-pubsub:peer:error') - -const { RPC } = require('./message') - -/** - * The known state of a connected peer. - */ -class Peer extends EventEmitter { - /** - * @param {PeerId} id - * @param {Array} protocols - */ - constructor ({ id, protocols }) { - super() - - /** - * @type {PeerId} - */ - this.id = id - /** - * @type {string} - */ - this.protocols = protocols - /** - * @type {Connection} - */ - this.conn = null - /** - * @type {Set} - */ - this.topics = new Set() - /** - * @type {Pushable} - */ - this.stream = null - } - - /** - * Is the peer connected currently? - * - * @type {boolean} - */ - get isConnected () { - return Boolean(this.conn) - } - - /** - * Do we have a connection to write on? - * - * @type {boolean} - */ - get isWritable () { - return Boolean(this.stream) - } - - /** - * Send a message to this peer. - * Throws if there is no `stream` to write to available. - * - * @param {Uint8Array} msg - * @returns {undefined} - */ - write (msg) { - if (!this.isWritable) { - const id = this.id.toB58String() - throw new Error('No writable connection to ' + id) - } - - this.stream.push(msg) - } - - /** - * Attach the peer to a connection and setup a write stream - * - * @param {Connection} conn - * @returns {void} - */ - async attachConnection (conn) { - const _prevStream = this.stream - if (_prevStream) { - // End the stream without emitting a close event - await _prevStream.end(false) - } - - this.stream = pushable({ - onEnd: (emit) => { - // close readable side of the stream - this.conn.reset && this.conn.reset() - this.conn = null - this.stream = null - if (emit !== false) { - this.emit('close') - } - } - }) - this.conn = conn - - pipe( - this.stream, - lp.encode(), - conn - ).catch(err => { - log.error(err) - }) - - // Only emit if the connection is new - if (!_prevStream) { - this.emit('connection') - } - } - - _sendRawSubscriptions (topics, subscribe) { - if (topics.size === 0) { - return - } - - const subs = [] - topics.forEach((topic) => { - subs.push({ - subscribe: subscribe, - topicID: topic - }) - }) - - this.write(RPC.encode({ - subscriptions: subs - })) - } - - /** - * Send the given subscriptions to this peer. - * @param {Set|Array} topics - * @returns {undefined} - */ - sendSubscriptions (topics) { - this._sendRawSubscriptions(topics, true) - } - - /** - * Send the given unsubscriptions to this peer. - * @param {Set|Array} topics - * @returns {undefined} - */ - sendUnsubscriptions (topics) { - this._sendRawSubscriptions(topics, false) - } - - /** - * Send messages to this peer. - * - * @param {Array} msgs - * @returns {undefined} - */ - sendMessages (msgs) { - this.write(RPC.encode({ - msgs: msgs - })) - } - - /** - * Bulk process subscription updates. - * - * @param {Array} changes - * @returns {undefined} - */ - updateSubscriptions (changes) { - changes.forEach((subopt) => { - if (subopt.subscribe) { - this.topics.add(subopt.topicID) - } else { - this.topics.delete(subopt.topicID) - } - }) - } - - /** - * Closes the open connection to peer - * @returns {void} - */ - close () { - // End the pushable - if (this.stream) { - this.stream.end() - } - - this.conn = null - this.stream = null - this.emit('close') - } -} - -module.exports = Peer diff --git a/src/sign.ts b/src/sign.ts new file mode 100644 index 0000000000..e156cfff66 --- /dev/null +++ b/src/sign.ts @@ -0,0 +1,95 @@ +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toRpcMessage } from './utils.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import { keys } from '@libp2p/crypto' +import type { Message, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' +import { peerIdFromKeys } from '@libp2p/peer-id' + +export const SignPrefix = uint8ArrayFromString('libp2p-pubsub:') + +/** + * Signs the provided message with the given `peerId` + */ +export async function signMessage (peerId: PeerId, message: Message, encode: (rpc: PubSubRPCMessage) => Uint8Array) { + // Get the message in bytes, and prepend with the pubsub prefix + const bytes = uint8ArrayConcat([ + SignPrefix, + encode(toRpcMessage(message)) + ]) + + if (peerId.privateKey == null) { + throw new Error('Cannot sign message, no private key present') + } + + if (peerId.publicKey == null) { + throw new Error('Cannot sign message, no public key present') + } + + const privateKey = await keys.unmarshalPrivateKey(peerId.privateKey) + const signature = await privateKey.sign(bytes) + + const outputMessage: Message = { + ...message, + signature: signature, + key: peerId.publicKey + } + + return outputMessage +} + +/** + * Verifies the signature of the given message + */ +export async function verifySignature (message: Message, encode: (rpc: PubSubRPCMessage) => Uint8Array) { + if (message.signature == null) { + throw new Error('Message must contain a signature to be verified') + } + + if (message.from == null) { + throw new Error('Message must contain a from property to be verified') + } + + // Get message sans the signature + const bytes = uint8ArrayConcat([ + SignPrefix, + encode({ + ...toRpcMessage(message), + signature: undefined, + key: undefined + }) + ]) + + // Get the public key + const pubKeyBytes = await messagePublicKey(message) + const pubKey = keys.unmarshalPublicKey(pubKeyBytes) + + // verify the base message + return await pubKey.verify(bytes, message.signature) +} + +/** + * Returns the PublicKey associated with the given message. + * If no valid PublicKey can be retrieved an error will be returned. + */ +export async function messagePublicKey (message: Message) { + // should be available in the from property of the message (peer id) + if (message.from == null) { + throw new Error('Could not get the public key from the originator id') + } + + if (message.key != null) { + const keyPeerId = await peerIdFromKeys(message.key) + + if (keyPeerId.publicKey != null) { + return keyPeerId.publicKey + } + } + + if (message.from.publicKey != null) { + return message.from.publicKey + } + + // We couldn't validate pubkey is from the originator, error + throw new Error('Could not get the public key from the originator id') +} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 4f4119bac1..0000000000 --- a/src/utils.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict' - -const crypto = require('libp2p-crypto') -const multibase = require('multibase') -const uint8ArrayToString = require('uint8arrays/to-string') -const uint8ArrayFromString = require('uint8arrays/from-string') - -exports = module.exports - -/** - * Generatea random sequence number. - * - * @returns {Uint8Array} - * @private - */ -exports.randomSeqno = () => { - return crypto.randomBytes(8) -} - -/** - * Generate a message id, based on the `from` and `seqno`. - * - * @param {string} from - * @param {Uint8Array} seqno - * @returns {string} - * @private - */ -exports.msgId = (from, seqno) => { - return from + uint8ArrayToString(seqno, 'base16') -} - -/** - * Check if any member of the first set is also a member - * of the second set. - * - * @param {Set|Array} a - * @param {Set|Array} b - * @returns {boolean} - * @private - */ -exports.anyMatch = (a, b) => { - let bHas - if (Array.isArray(b)) { - bHas = (val) => b.indexOf(val) > -1 - } else { - bHas = (val) => b.has(val) - } - - for (const val of a) { - if (bHas(val)) { - return true - } - } - - return false -} - -/** - * Make everything an array. - * - * @param {any} maybeArray - * @returns {Array} - * @private - */ -exports.ensureArray = (maybeArray) => { - if (!Array.isArray(maybeArray)) { - return [maybeArray] - } - - return maybeArray -} - -/** - * Ensures `message.from` is base58 encoded - * @param {Object} message - * @param {Uint8Array|String} message.from - * @return {Object} - */ -exports.normalizeInRpcMessage = (message) => { - const m = Object.assign({}, message) - if (message.from instanceof Uint8Array) { - m.from = uint8ArrayToString(message.from, 'base58btc') - } - return m -} - -/** - * The same as `normalizeInRpcMessage`, but performed on an array of messages - * @param {Object[]} messages - * @return {Object[]} - */ -exports.normalizeInRpcMessages = (messages) => { - if (!messages) { - return messages - } - return messages.map(exports.normalizeInRpcMessage) -} - -exports.normalizeOutRpcMessage = (message) => { - const m = Object.assign({}, message) - if (typeof message.from === 'string' || message.from instanceof String) { - m.from = multibase.decode('z' + message.from) - } - if (typeof message.data === 'string' || message.data instanceof String) { - m.data = uint8ArrayFromString(message.data) - } - return m -} - -exports.normalizeOutRpcMessages = (messages) => { - if (!messages) { - return messages - } - return messages.map(exports.normalizeOutRpcMessage) -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000000..1d9e6df351 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,107 @@ +import { randomBytes } from 'iso-random-stream' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { sha256 } from 'multiformats/hashes/sha2' +import type { Message, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' +import { peerIdFromBytes } from '@libp2p/peer-id' +import { codes } from './errors.js' +import errcode from 'err-code' + +/** + * Generate a random sequence number + */ +export function randomSeqno (): bigint { + return BigInt(`0x${uint8ArrayToString(randomBytes(8), 'base16')}`) +} + +/** + * Generate a message id, based on the `key` and `seqno` + */ +export const msgId = (key: Uint8Array, seqno: bigint) => { + const seqnoBytes = uint8ArrayFromString(seqno.toString(16).padStart(16, '0'), 'base16') + + const msgId = new Uint8Array(key.length + seqnoBytes.length) + msgId.set(key, 0) + msgId.set(seqnoBytes, key.length) + + return msgId +} + +/** + * Generate a message id, based on message `data` + */ +export const noSignMsgId = (data: Uint8Array) => { + return sha256.encode(data) +} + +/** + * Check if any member of the first set is also a member + * of the second set + */ +export const anyMatch = (a: Set | number[], b: Set | number[]) => { + let bHas + if (Array.isArray(b)) { + bHas = (val: number) => b.includes(val) + } else { + bHas = (val: number) => b.has(val) + } + + for (const val of a) { + if (bHas(val)) { + return true + } + } + + return false +} + +/** + * Make everything an array + */ +export const ensureArray = function (maybeArray: T | T[]) { + if (!Array.isArray(maybeArray)) { + return [maybeArray] + } + + return maybeArray +} + +export const toMessage = (message: PubSubRPCMessage): Message => { + if (message.from == null) { + throw errcode(new Error('RPC message was missing from'), codes.ERR_MISSING_FROM) + } + + return { + from: peerIdFromBytes(message.from), + topic: message.topic ?? '', + sequenceNumber: message.sequenceNumber == null ? undefined : bigIntFromBytes(message.sequenceNumber), + data: message.data ?? new Uint8Array(0), + signature: message.signature ?? undefined, + key: message.key ?? undefined + } +} + +export const toRpcMessage = (message: Message): PubSubRPCMessage => { + return { + from: message.from.multihash.bytes, + data: message.data, + sequenceNumber: message.sequenceNumber == null ? undefined : bigIntToBytes(message.sequenceNumber), + topic: message.topic, + signature: message.signature, + key: message.key + } +} + +export const bigIntToBytes = (num: bigint): Uint8Array => { + let str = num.toString(16) + + if (str.length % 2 !== 0) { + str = `0${str}` + } + + return uint8ArrayFromString(str, 'base16') +} + +export const bigIntFromBytes = (num: Uint8Array): bigint => { + return BigInt(`0x${uint8ArrayToString(num, 'base16')}`) +} diff --git a/test/emit-self.spec.ts b/test/emit-self.spec.ts new file mode 100644 index 0000000000..438134b8cf --- /dev/null +++ b/test/emit-self.spec.ts @@ -0,0 +1,108 @@ +import { expect } from 'aegir/chai' +import { + createPeerId, + MockRegistrar, + PubsubImplementation +} from './utils/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import delay from 'delay' +import { Components } from '@libp2p/interfaces/components' + +const protocol = '/pubsub/1.0.0' +const topic = 'foo' +const data = uint8ArrayFromString('bar') +const shouldNotHappen = () => expect.fail() + +describe('emitSelf', () => { + let pubsub: PubsubImplementation + + describe('enabled', () => { + before(async () => { + const peerId = await createPeerId() + + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + emitSelf: true + }) + pubsub.init(new Components({ + peerId, + registrar: new MockRegistrar() + })) + }) + + before(async () => { + await pubsub.start() + pubsub.subscribe(topic) + }) + + after(async () => { + await pubsub.stop() + }) + + it('should emit to self on publish', async () => { + pubsub.subscribe(topic) + + const promise = new Promise((resolve) => { + pubsub.addEventListener('message', (evt) => { + if (evt.detail.topic === topic) { + resolve() + } + }) + }) + + await pubsub.publish(topic, data) + + return await promise + }) + + it('should publish a message without data', async () => { + pubsub.subscribe(topic) + + const promise = new Promise((resolve) => { + pubsub.addEventListener('message', (evt) => { + if (evt.detail.topic === topic) { + resolve() + } + }) + }) + + await pubsub.publish(topic) + + return await promise + }) + }) + + describe('disabled', () => { + before(async () => { + const peerId = await createPeerId() + + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + emitSelf: false + }) + pubsub.init(new Components({ + peerId, + registrar: new MockRegistrar() + })) + }) + + before(async () => { + await pubsub.start() + pubsub.subscribe(topic) + }) + + after(async () => { + await pubsub.stop() + }) + + it('should not emit to self on publish', async () => { + pubsub.subscribe(topic) + pubsub.addEventListener('message', shouldNotHappen) + + await pubsub.publish(topic, data) + + // Wait 1 second to guarantee that self is not noticed + await delay(1000) + }) + }) +}) diff --git a/test/instance.spec.js b/test/instance.spec.js deleted file mode 100644 index 9a9760f34a..0000000000 --- a/test/instance.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-spies')) -const expect = chai.expect - -const PubsubBaseProtocol = require('../src') -const { createPeerId, mockRegistrar } = require('./utils') - -describe('should validate instance parameters', () => { - let peerId - - before(async () => { - peerId = await createPeerId() - }) - - it('should throw if no debugName is provided', () => { - expect(() => { - new PubsubBaseProtocol() // eslint-disable-line no-new - }).to.throw() - }) - - it('should throw if no multicodec is provided', () => { - expect(() => { - new PubsubBaseProtocol({ // eslint-disable-line no-new - debugName: 'pubsub' - }) - }).to.throw() - }) - - it('should throw if no peerId is provided', () => { - expect(() => { - new PubsubBaseProtocol({ // eslint-disable-line no-new - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0' - }) - }).to.throw() - }) - - it('should throw if an invalid peerId is provided', () => { - expect(() => { - new PubsubBaseProtocol({ // eslint-disable-line no-new - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - peerId: 'fake-peer-id' - }) - }).to.throw() - }) - - it('should throw if no registrar object is provided', () => { - expect(() => { - new PubsubBaseProtocol({ // eslint-disable-line no-new - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - peerId: peerId - }) - }).to.throw() - }) - - it('should accept valid parameters', () => { - expect(() => { - new PubsubBaseProtocol({ // eslint-disable-line no-new - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - peerId: peerId, - registrar: mockRegistrar - }) - }).not.to.throw() - }) -}) diff --git a/test/instance.spec.ts b/test/instance.spec.ts new file mode 100644 index 0000000000..2b153a3bc7 --- /dev/null +++ b/test/instance.spec.ts @@ -0,0 +1,42 @@ +import { expect } from 'aegir/chai' +import { PubSubBaseProtocol } from '../src/index.js' +import type { PublishResult, PubSubRPC, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' + +class PubsubProtocol extends PubSubBaseProtocol { + decodeRpc (bytes: Uint8Array): PubSubRPC { + throw new Error('Method not implemented.') + } + + encodeRpc (rpc: PubSubRPC): Uint8Array { + throw new Error('Method not implemented.') + } + + decodeMessage (bytes: Uint8Array): PubSubRPCMessage { + throw new Error('Method not implemented.') + } + + encodeMessage (rpc: PubSubRPCMessage): Uint8Array { + throw new Error('Method not implemented.') + } + + async publishMessage (): Promise { + throw new Error('Method not implemented.') + } +} + +describe('pubsub instance', () => { + it('should throw if no init is provided', () => { + expect(() => { + // @ts-expect-error incorrect constructor args + new PubsubProtocol() // eslint-disable-line no-new + }).to.throw() + }) + + it('should accept valid parameters', () => { + expect(() => { + new PubsubProtocol({ // eslint-disable-line no-new + multicodecs: ['/pubsub/1.0.0'] + }) + }).not.to.throw() + }) +}) diff --git a/test/lifecycle.spec.ts b/test/lifecycle.spec.ts new file mode 100644 index 0000000000..49ecb4b79b --- /dev/null +++ b/test/lifecycle.spec.ts @@ -0,0 +1,261 @@ +import { expect } from 'aegir/chai' +import sinon from 'sinon' +import { PubSubBaseProtocol } from '../src/index.js' +import { + createPeerId, + PubsubImplementation, + ConnectionPair, + MockRegistrar, + mockIncomingStreamEvent +} from './utils/index.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { Registrar } from '@libp2p/interfaces/registrar' +import type { PublishResult, PubSubRPC, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' +import { Components } from '@libp2p/interfaces/components' + +class PubsubProtocol extends PubSubBaseProtocol { + decodeRpc (bytes: Uint8Array): PubSubRPC { + throw new Error('Method not implemented.') + } + + encodeRpc (rpc: PubSubRPC): Uint8Array { + throw new Error('Method not implemented.') + } + + decodeMessage (bytes: Uint8Array): PubSubRPCMessage { + throw new Error('Method not implemented.') + } + + encodeMessage (rpc: PubSubRPCMessage): Uint8Array { + throw new Error('Method not implemented.') + } + + async publishMessage (): Promise { + throw new Error('Method not implemented.') + } +} + +describe('pubsub base lifecycle', () => { + describe('should start and stop properly', () => { + let pubsub: PubsubProtocol + let sinonMockRegistrar: Registrar + + beforeEach(async () => { + const peerId = await createPeerId() + // @ts-expect-error incomplete implementation + sinonMockRegistrar = { + handle: sinon.stub(), + unhandle: sinon.stub(), + register: sinon.stub().returns(`id-${Math.random()}`), + unregister: sinon.stub() + } + + pubsub = new PubsubProtocol({ + multicodecs: ['/pubsub/1.0.0'] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: sinonMockRegistrar + })) + + expect(pubsub.peers.size).to.be.eql(0) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should be able to start and stop', async () => { + await pubsub.start() + expect(sinonMockRegistrar.handle).to.have.property('calledOnce', true) + expect(sinonMockRegistrar.register).to.have.property('calledOnce', true) + + await pubsub.stop() + expect(sinonMockRegistrar.unhandle).to.have.property('calledOnce', true) + expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', true) + }) + + it('starting should not throw if already started', async () => { + await pubsub.start() + await pubsub.start() + expect(sinonMockRegistrar.handle).to.have.property('calledOnce', true) + expect(sinonMockRegistrar.register).to.have.property('calledOnce', true) + + await pubsub.stop() + expect(sinonMockRegistrar.unhandle).to.have.property('calledOnce', true) + expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', true) + }) + + it('stopping should not throw if not started', async () => { + await pubsub.stop() + expect(sinonMockRegistrar.handle).to.have.property('calledOnce', false) + expect(sinonMockRegistrar.unhandle).to.have.property('calledOnce', false) + expect(sinonMockRegistrar.register).to.have.property('calledOnce', false) + expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', false) + }) + }) + + describe('should be able to register two nodes', () => { + const protocol = '/pubsub/1.0.0' + let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation + let peerIdA: PeerId, peerIdB: PeerId + let registrarA: MockRegistrar + let registrarB: MockRegistrar + + // mount pubsub + beforeEach(async () => { + peerIdA = await createPeerId() + peerIdB = await createPeerId() + + registrarA = new MockRegistrar() + registrarB = new MockRegistrar() + + pubsubA = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsubA.init(new Components({ + peerId: peerIdA, + registrar: registrarA + })) + pubsubB = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsubB.init(new Components({ + peerId: peerIdB, + registrar: registrarB + })) + }) + + // start pubsub + beforeEach(async () => { + await Promise.all([ + pubsubA.start(), + pubsubB.start() + ]) + + expect(registrarA.getHandler(protocol)).to.be.ok() + expect(registrarB.getHandler(protocol)).to.be.ok() + }) + + afterEach(async () => { + sinon.restore() + + await Promise.all([ + pubsubA.stop(), + pubsubB.stop() + ]) + }) + + it('should handle onConnect as expected', async () => { + const topologyA = registrarA.getTopologies(protocol)[0] + const handlerB = registrarB.getHandler(protocol) + + if (topologyA == null || handlerB == null) { + throw new Error(`No handler registered for ${protocol}`) + } + + const [c0, c1] = ConnectionPair() + + // Notify peers of connection + await topologyA.onConnect(peerIdB, c0) + await handlerB(await mockIncomingStreamEvent(protocol, c1, peerIdA)) + + expect(pubsubA.peers.size).to.be.eql(1) + expect(pubsubB.peers.size).to.be.eql(1) + }) + + it('should use the latest connection if onConnect is called more than once', async () => { + const topologyA = registrarA.getTopologies(protocol)[0] + const handlerB = registrarB.getHandler(protocol) + + if (topologyA == null || handlerB == null) { + throw new Error(`No handler registered for ${protocol}`) + } + + // Notify peers of connection + const [c0, c1] = ConnectionPair() + const [c2] = ConnectionPair() + + sinon.spy(c0, 'newStream') + + await topologyA.onConnect(peerIdB, c0) + await handlerB(await mockIncomingStreamEvent(protocol, c1, peerIdA)) + expect(c0.newStream).to.have.property('callCount', 1) + + // @ts-expect-error _removePeer is a protected method + sinon.spy(pubsubA, '_removePeer') + + sinon.spy(c2, 'newStream') + + await topologyA?.onConnect(peerIdB, c2) + expect(c2.newStream).to.have.property('callCount', 1) + + // @ts-expect-error _removePeer is a protected method + expect(pubsubA._removePeer).to.have.property('callCount', 0) + + // Verify the first stream was closed + // @ts-expect-error .returnValues is a sinon property + const { stream: firstStream } = await c0.newStream.returnValues[0] + try { + await firstStream.sink(['test']) + } catch (err: any) { + expect(err).to.exist() + return + } + expect.fail('original stream should have ended') + }) + + it('should handle newStream errors in onConnect', async () => { + const topologyA = registrarA.getTopologies(protocol)[0] + const handlerB = registrarB.getHandler(protocol) + + if (topologyA == null || handlerB == null) { + throw new Error(`No handler registered for ${protocol}`) + } + + // Notify peers of connection + const [c0, c1] = ConnectionPair() + const error = new Error('new stream error') + sinon.stub(c0, 'newStream').throws(error) + + await topologyA.onConnect(peerIdB, c0) + await handlerB(await mockIncomingStreamEvent(protocol, c1, peerIdA)) + + expect(c0.newStream).to.have.property('callCount', 1) + }) + + it('should handle onDisconnect as expected', async () => { + const topologyA = registrarA.getTopologies(protocol)[0] + const topologyB = registrarB.getTopologies(protocol)[0] + const handlerB = registrarB.getHandler(protocol) + + if (topologyA == null || handlerB == null) { + throw new Error(`No handler registered for ${protocol}`) + } + + // Notify peers of connection + const [c0, c1] = ConnectionPair() + + await topologyA.onConnect(peerIdB, c0) + await handlerB(await mockIncomingStreamEvent(protocol, c1, peerIdA)) + + // Notice peers of disconnect + topologyA?.onDisconnect(peerIdB) + topologyB?.onDisconnect(peerIdA) + + expect(pubsubA.peers.size).to.be.eql(0) + expect(pubsubB.peers.size).to.be.eql(0) + }) + + it('should handle onDisconnect for unknown peers', () => { + const topologyA = registrarA.getTopologies(protocol)[0] + + expect(pubsubA.peers.size).to.be.eql(0) + + // Notice peers of disconnect + topologyA?.onDisconnect(peerIdB) + + expect(pubsubA.peers.size).to.be.eql(0) + }) + }) +}) diff --git a/test/message.spec.ts b/test/message.spec.ts new file mode 100644 index 0000000000..efab59d70d --- /dev/null +++ b/test/message.spec.ts @@ -0,0 +1,91 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { + createPeerId, + MockRegistrar, + PubsubImplementation +} from './utils/index.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { Message } from '@libp2p/interfaces/pubsub' +import { Components } from '@libp2p/interfaces/components' + +describe('pubsub base messages', () => { + let peerId: PeerId + let pubsub: PubsubImplementation + + before(async () => { + peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: ['/pubsub/1.0.0'] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + }) + + afterEach(() => { + sinon.restore() + }) + + it('buildMessage normalizes and signs messages', async () => { + const message: Message = { + from: peerId, + data: uint8ArrayFromString('hello'), + topic: 'test-topic' + } + + const signedMessage = await pubsub.buildMessage(message) + + await expect(pubsub.validate(signedMessage)).to.eventually.not.be.rejected() + }) + + it('validate with StrictNoSign will reject a message with from, signature, key, seqno present', async () => { + const message: Message = { + from: peerId, + data: uint8ArrayFromString('hello'), + topic: 'test-topic' + } + + sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictSign') + + const signedMessage = await pubsub.buildMessage(message) + + sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign') + await expect(pubsub.validate(signedMessage)).to.eventually.be.rejected() + // @ts-expect-error this field is not optional + delete signedMessage.from + await expect(pubsub.validate(signedMessage)).to.eventually.be.rejected() + delete signedMessage.signature + await expect(pubsub.validate(signedMessage)).to.eventually.be.rejected() + delete signedMessage.key + await expect(pubsub.validate(signedMessage)).to.eventually.be.rejected() + delete signedMessage.sequenceNumber + await expect(pubsub.validate(signedMessage)).to.eventually.not.be.rejected() + }) + + it('validate with StrictNoSign will validate a message without a signature, key, and seqno', async () => { + const message: Message = { + from: peerId, + data: uint8ArrayFromString('hello'), + topic: 'test-topic' + } + + sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign') + + const signedMessage = await pubsub.buildMessage(message) + await expect(pubsub.validate(signedMessage)).to.eventually.not.be.rejected() + }) + + it('validate with StrictSign requires a signature', async () => { + const message: Message = { + from: peerId, + data: uint8ArrayFromString('hello'), + topic: 'test-topic' + } + + await expect(pubsub.validate(message)).to.be.rejectedWith(Error, 'Signing required and no signature was present') + }) +}) diff --git a/test/message/rpc.proto b/test/message/rpc.proto new file mode 100644 index 0000000000..5e32c35a5c --- /dev/null +++ b/test/message/rpc.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +message RPC { + repeated SubOpts subscriptions = 1; + repeated Message messages = 2; + optional ControlMessage control = 3; + + message SubOpts { + optional bool subscribe = 1; // subscribe or unsubcribe + optional string topic = 2; + } + + message Message { + optional bytes from = 1; + optional bytes data = 2; + optional bytes seqno = 3; + optional string topic = 4; + optional bytes signature = 5; + optional bytes key = 6; + } +} + +message ControlMessage { + repeated ControlIHave ihave = 1; + repeated ControlIWant iwant = 2; + repeated ControlGraft graft = 3; + repeated ControlPrune prune = 4; +} + +message ControlIHave { + optional string topic = 1; + repeated bytes messageIDs = 2; +} + +message ControlIWant { + repeated bytes messageIDs = 1; +} + +message ControlGraft { + optional string topic = 1; +} + +message ControlPrune { + optional string topic = 1; + repeated PeerInfo peers = 2; + optional uint64 backoff = 3; +} + +message PeerInfo { + optional bytes peerID = 1; + optional bytes signedPeerRecord = 2; +} diff --git a/test/message/rpc.ts b/test/message/rpc.ts new file mode 100644 index 0000000000..4440d48a07 --- /dev/null +++ b/test/message/rpc.ts @@ -0,0 +1,215 @@ +/* eslint-disable import/export */ +/* eslint-disable @typescript-eslint/no-namespace */ + +import { encodeMessage, decodeMessage, message, bool, string, bytes, uint64 } from 'protons-runtime' +import type { Codec } from 'protons-runtime' + +export interface RPC { + subscriptions: RPC.SubOpts[] + messages: RPC.Message[] + control?: ControlMessage +} + +export namespace RPC { + export interface SubOpts { + subscribe?: boolean + topic?: string + } + + export namespace SubOpts { + export const codec = (): Codec => { + return message({ + 1: { name: 'subscribe', codec: bool, optional: true }, + 2: { name: 'topic', codec: string, optional: true } + }) + } + + export const encode = (obj: SubOpts): Uint8Array => { + return encodeMessage(obj, SubOpts.codec()) + } + + export const decode = (buf: Uint8Array): SubOpts => { + return decodeMessage(buf, SubOpts.codec()) + } + } + + export interface Message { + from?: Uint8Array + data?: Uint8Array + seqno?: Uint8Array + topic?: string + signature?: Uint8Array + key?: Uint8Array + } + + export namespace Message { + export const codec = (): Codec => { + return message({ + 1: { name: 'from', codec: bytes, optional: true }, + 2: { name: 'data', codec: bytes, optional: true }, + 3: { name: 'seqno', codec: bytes, optional: true }, + 4: { name: 'topic', codec: string, optional: true }, + 5: { name: 'signature', codec: bytes, optional: true }, + 6: { name: 'key', codec: bytes, optional: true } + }) + } + + export const encode = (obj: Message): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array): Message => { + return decodeMessage(buf, Message.codec()) + } + } + + export const codec = (): Codec => { + return message({ + 1: { name: 'subscriptions', codec: RPC.SubOpts.codec(), repeats: true }, + 2: { name: 'messages', codec: RPC.Message.codec(), repeats: true }, + 3: { name: 'control', codec: ControlMessage.codec(), optional: true } + }) + } + + export const encode = (obj: RPC): Uint8Array => { + return encodeMessage(obj, RPC.codec()) + } + + export const decode = (buf: Uint8Array): RPC => { + return decodeMessage(buf, RPC.codec()) + } +} + +export interface ControlMessage { + ihave: ControlIHave[] + iwant: ControlIWant[] + graft: ControlGraft[] + prune: ControlPrune[] +} + +export namespace ControlMessage { + export const codec = (): Codec => { + return message({ + 1: { name: 'ihave', codec: ControlIHave.codec(), repeats: true }, + 2: { name: 'iwant', codec: ControlIWant.codec(), repeats: true }, + 3: { name: 'graft', codec: ControlGraft.codec(), repeats: true }, + 4: { name: 'prune', codec: ControlPrune.codec(), repeats: true } + }) + } + + export const encode = (obj: ControlMessage): Uint8Array => { + return encodeMessage(obj, ControlMessage.codec()) + } + + export const decode = (buf: Uint8Array): ControlMessage => { + return decodeMessage(buf, ControlMessage.codec()) + } +} + +export interface ControlIHave { + topic?: string + messageIDs: Uint8Array[] +} + +export namespace ControlIHave { + export const codec = (): Codec => { + return message({ + 1: { name: 'topic', codec: string, optional: true }, + 2: { name: 'messageIDs', codec: bytes, repeats: true } + }) + } + + export const encode = (obj: ControlIHave): Uint8Array => { + return encodeMessage(obj, ControlIHave.codec()) + } + + export const decode = (buf: Uint8Array): ControlIHave => { + return decodeMessage(buf, ControlIHave.codec()) + } +} + +export interface ControlIWant { + messageIDs: Uint8Array[] +} + +export namespace ControlIWant { + export const codec = (): Codec => { + return message({ + 1: { name: 'messageIDs', codec: bytes, repeats: true } + }) + } + + export const encode = (obj: ControlIWant): Uint8Array => { + return encodeMessage(obj, ControlIWant.codec()) + } + + export const decode = (buf: Uint8Array): ControlIWant => { + return decodeMessage(buf, ControlIWant.codec()) + } +} + +export interface ControlGraft { + topic?: string +} + +export namespace ControlGraft { + export const codec = (): Codec => { + return message({ + 1: { name: 'topic', codec: string, optional: true } + }) + } + + export const encode = (obj: ControlGraft): Uint8Array => { + return encodeMessage(obj, ControlGraft.codec()) + } + + export const decode = (buf: Uint8Array): ControlGraft => { + return decodeMessage(buf, ControlGraft.codec()) + } +} + +export interface ControlPrune { + topic?: string + peers: PeerInfo[] + backoff?: bigint +} + +export namespace ControlPrune { + export const codec = (): Codec => { + return message({ + 1: { name: 'topic', codec: string, optional: true }, + 2: { name: 'peers', codec: PeerInfo.codec(), repeats: true }, + 3: { name: 'backoff', codec: uint64, optional: true } + }) + } + + export const encode = (obj: ControlPrune): Uint8Array => { + return encodeMessage(obj, ControlPrune.codec()) + } + + export const decode = (buf: Uint8Array): ControlPrune => { + return decodeMessage(buf, ControlPrune.codec()) + } +} + +export interface PeerInfo { + peerID?: Uint8Array + signedPeerRecord?: Uint8Array +} + +export namespace PeerInfo { + export const codec = (): Codec => { + return message({ + 1: { name: 'peerID', codec: bytes, optional: true }, + 2: { name: 'signedPeerRecord', codec: bytes, optional: true } + }) + } + + export const encode = (obj: PeerInfo): Uint8Array => { + return encodeMessage(obj, PeerInfo.codec()) + } + + export const decode = (buf: Uint8Array): PeerInfo => { + return decodeMessage(buf, PeerInfo.codec()) + } +} diff --git a/test/pubsub.spec.js b/test/pubsub.spec.js deleted file mode 100644 index 2f40dd4fdd..0000000000 --- a/test/pubsub.spec.js +++ /dev/null @@ -1,355 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-spies')) -const expect = chai.expect -const sinon = require('sinon') - -const PubsubBaseProtocol = require('../src') -const Peer = require('../src/peer') -const { randomSeqno } = require('../src/utils') -const { - createPeerId, - createMockRegistrar, - mockRegistrar, - PubsubImplementation, - ConnectionPair -} = require('./utils') - -describe('pubsub base protocol', () => { - describe('should start and stop properly', () => { - let pubsub - let sinonMockRegistrar - - beforeEach(async () => { - const peerId = await createPeerId() - sinonMockRegistrar = { - handle: sinon.stub(), - register: sinon.stub(), - unregister: sinon.stub() - } - - pubsub = new PubsubBaseProtocol({ - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - peerId: peerId, - registrar: sinonMockRegistrar - }) - - expect(pubsub.peers.size).to.be.eql(0) - }) - - afterEach(() => { - sinon.restore() - }) - - it('should be able to start and stop', async () => { - await pubsub.start() - expect(sinonMockRegistrar.handle.calledOnce).to.be.true() - expect(sinonMockRegistrar.register.calledOnce).to.be.true() - - await pubsub.stop() - expect(sinonMockRegistrar.unregister.calledOnce).to.be.true() - }) - - it('starting should not throw if already started', async () => { - await pubsub.start() - await pubsub.start() - expect(sinonMockRegistrar.handle.calledOnce).to.be.true() - expect(sinonMockRegistrar.register.calledOnce).to.be.true() - - await pubsub.stop() - expect(sinonMockRegistrar.unregister.calledOnce).to.be.true() - }) - - it('stopping should not throw if not started', async () => { - await pubsub.stop() - expect(sinonMockRegistrar.register.calledOnce).to.be.false() - expect(sinonMockRegistrar.unregister.calledOnce).to.be.false() - }) - }) - - describe('should handle message creation and signing', () => { - let peerId - let pubsub - - before(async () => { - peerId = await createPeerId() - pubsub = new PubsubBaseProtocol({ - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - peerId: peerId, - registrar: mockRegistrar - }) - }) - - afterEach(() => { - sinon.restore() - }) - - it('_buildMessage normalizes and signs messages', async () => { - const message = { - from: peerId.id, - data: 'hello', - seqno: randomSeqno(), - topicIDs: ['test-topic'] - } - - const signedMessage = await pubsub._buildMessage(message) - const verified = await pubsub.validate(signedMessage) - - expect(verified).to.eql(true) - }) - - it('validate with strict signing off will validate a present signature', async () => { - const message = { - from: peerId.id, - data: 'hello', - seqno: randomSeqno(), - topicIDs: ['test-topic'] - } - - sinon.stub(pubsub, 'strictSigning').value(false) - - const signedMessage = await pubsub._buildMessage(message) - const verified = await pubsub.validate(signedMessage) - - expect(verified).to.eql(true) - }) - - it('validate with strict signing requires a signature', async () => { - const message = { - from: peerId.id, - data: 'hello', - seqno: randomSeqno(), - topicIDs: ['test-topic'] - } - - const verified = await pubsub.validate(message) - - expect(verified).to.eql(false) - }) - }) - - describe('should be able to register two nodes', () => { - const protocol = '/pubsub/1.0.0' - let pubsubA, pubsubB - let peerIdA, peerIdB - const registrarRecordA = {} - const registrarRecordB = {} - - // mount pubsub - beforeEach(async () => { - peerIdA = await createPeerId() - peerIdB = await createPeerId() - - pubsubA = new PubsubImplementation(protocol, peerIdA, createMockRegistrar(registrarRecordA)) - pubsubB = new PubsubImplementation(protocol, peerIdB, createMockRegistrar(registrarRecordB)) - }) - - // start pubsub - beforeEach(async () => { - await Promise.all([ - pubsubA.start(), - pubsubB.start() - ]) - - expect(Object.keys(registrarRecordA)).to.have.lengthOf(1) - expect(Object.keys(registrarRecordB)).to.have.lengthOf(1) - }) - - afterEach(() => { - sinon.restore() - - return Promise.all([ - pubsubA.stop(), - pubsubB.stop() - ]) - }) - - it('should handle onConnect as expected', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler - - // Notice peers of connection - const [c0, c1] = ConnectionPair() - - await onConnectA(peerIdB, c0) - await handlerB({ - protocol, - stream: c1.stream, - connection: { - remotePeer: peerIdA - } - }) - - expect(pubsubA.peers.size).to.be.eql(1) - expect(pubsubB.peers.size).to.be.eql(1) - }) - - it('should use the latest connection if onConnect is called more than once', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler - - // Notice peers of connection - const [c0, c1] = ConnectionPair() - const [c2] = ConnectionPair() - - sinon.spy(c0, 'newStream') - - await onConnectA(peerIdB, c0) - await handlerB({ - protocol, - stream: c1.stream, - connection: { - remotePeer: peerIdA - } - }) - expect(c0.newStream).to.have.property('callCount', 1) - - sinon.spy(pubsubA, '_removePeer') - - sinon.spy(c2, 'newStream') - - await onConnectA(peerIdB, c2) - expect(c2.newStream).to.have.property('callCount', 1) - expect(pubsubA._removePeer).to.have.property('callCount', 0) - - // Verify the first stream was closed - const { stream: firstStream } = await c0.newStream.returnValues[0] - try { - await firstStream.sink(['test']) - } catch (err) { - expect(err).to.exist() - return - } - expect.fail('original stream should have ended') - }) - - it('should handle newStream errors in onConnect', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler - - // Notice peers of connection - const [c0, c1] = ConnectionPair() - const error = new Error('new stream error') - sinon.stub(c0, 'newStream').throws(error) - - await onConnectA(peerIdB, c0) - await handlerB({ - protocol, - stream: c1.stream, - connection: { - remotePeer: peerIdA - } - }) - - expect(c0.newStream).to.have.property('callCount', 1) - }) - - it('should handle onDisconnect as expected', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const onDisconnectA = registrarRecordA[protocol].onDisconnect - const handlerB = registrarRecordB[protocol].handler - const onDisconnectB = registrarRecordB[protocol].onDisconnect - - // Notice peers of connection - const [c0, c1] = ConnectionPair() - - await onConnectA(peerIdB, c0) - await handlerB({ - protocol, - stream: c1.stream, - connection: { - remotePeer: peerIdA - } - }) - - // Notice peers of disconnect - onDisconnectA(peerIdB) - onDisconnectB(peerIdA) - - expect(pubsubA.peers.size).to.be.eql(0) - expect(pubsubB.peers.size).to.be.eql(0) - }) - - it('should handle onDisconnect for unknown peers', () => { - const onDisconnectA = registrarRecordA[protocol].onDisconnect - - expect(pubsubA.peers.size).to.be.eql(0) - - // Notice peers of disconnect - onDisconnectA(peerIdB) - - expect(pubsubA.peers.size).to.be.eql(0) - }) - }) - - describe('getSubscribers', () => { - let peerId - let pubsub - - beforeEach(async () => { - peerId = await createPeerId() - pubsub = new PubsubBaseProtocol({ - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - peerId: peerId, - registrar: mockRegistrar - }) - }) - - afterEach(() => pubsub.stop()) - - it('should fail if pubsub is not started', () => { - const topic = 'topic-test' - - try { - pubsub.getSubscribers(topic) - } catch (err) { - expect(err).to.exist() - expect(err.code).to.eql('ERR_NOT_STARTED_YET') - return - } - throw new Error('should fail if pubsub is not started') - }) - - it('should fail if no topic is provided', async () => { - // start pubsub - await pubsub.start() - - try { - pubsub.getSubscribers() - } catch (err) { - expect(err).to.exist() - expect(err.code).to.eql('ERR_NOT_VALID_TOPIC') - return - } - throw new Error('should fail if no topic is provided') - }) - - it('should get peer subscribed to one topic', async () => { - const topic = 'topic-test' - - // start pubsub - await pubsub.start() - - let peersSubscribed = pubsub.getSubscribers(topic) - expect(peersSubscribed).to.be.empty() - - // Set mock peer subscribed - const peer = new Peer({ id: peerId }) - const id = peer.id.toB58String() - - peer.topics.add(topic) - pubsub.peers.set(id, peer) - - peersSubscribed = pubsub.getSubscribers(topic) - - expect(peersSubscribed).to.not.be.empty() - expect(peersSubscribed[0]).to.eql(id) - }) - }) -}) diff --git a/test/pubsub.spec.ts b/test/pubsub.spec.ts new file mode 100644 index 0000000000..0d8a4ecdfa --- /dev/null +++ b/test/pubsub.spec.ts @@ -0,0 +1,505 @@ +/* eslint max-nested-callbacks: ["error", 6] */ +import { expect } from 'aegir/chai' +import sinon from 'sinon' +import pWaitFor from 'p-wait-for' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PeerStreams } from '../src/peer-streams.js' +import { + createPeerId, + MockRegistrar, + ConnectionPair, + PubsubImplementation, + mockIncomingStreamEvent +} from './utils/index.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import { PeerSet } from '@libp2p/peer-collections' +import { Components } from '@libp2p/interfaces/components' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { noSignMsgId } from '../src/utils.js' +import type { PubSubRPC } from '@libp2p/interfaces/src/pubsub' +import delay from 'delay' +import pDefer from 'p-defer' + +const protocol = '/pubsub/1.0.0' +const topic = 'test-topic' +const message = uint8ArrayFromString('hello') + +describe('pubsub base implementation', () => { + describe('publish', () => { + let pubsub: PubsubImplementation + + beforeEach(async () => { + const peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + }) + + afterEach(async () => await pubsub.stop()) + + it('calls _publish for router to forward messages', async () => { + sinon.spy(pubsub, 'publishMessage') + + await pubsub.start() + await pubsub.publish(topic, message) + + // event dispatch is async + await pWaitFor(() => { + // @ts-expect-error .callCount is a added by sinon + return pubsub.publishMessage.callCount === 1 + }) + + // @ts-expect-error .callCount is a added by sinon + expect(pubsub.publishMessage.callCount).to.eql(1) + }) + + it('should sign messages on publish', async () => { + sinon.spy(pubsub, 'publishMessage') + + await pubsub.start() + await pubsub.publish(topic, message) + + // event dispatch is async + await pWaitFor(() => { + // @ts-expect-error .callCount is a added by sinon + return pubsub.publishMessage.callCount === 1 + }) + + // Get the first message sent to _publish, and validate it + // @ts-expect-error .getCall is a added by sinon + const signedMessage: Message = pubsub.publishMessage.getCall(0).lastArg + + await expect(pubsub.validate(signedMessage)).to.eventually.be.undefined() + }) + }) + + describe('subscribe', () => { + describe('basics', () => { + let pubsub: PubsubImplementation + + beforeEach(async () => { + const peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + await pubsub.start() + }) + + afterEach(async () => await pubsub.stop()) + + it('should add subscription', () => { + pubsub.subscribe(topic) + + expect(pubsub.subscriptions.size).to.eql(1) + expect(pubsub.subscriptions.has(topic)).to.be.true() + }) + }) + + describe('two nodes', () => { + let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation + let peerIdA: PeerId, peerIdB: PeerId + let registrarA: MockRegistrar + let registrarB: MockRegistrar + + beforeEach(async () => { + peerIdA = await createPeerId() + peerIdB = await createPeerId() + + registrarA = new MockRegistrar() + registrarB = new MockRegistrar() + + pubsubA = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsubA.init(new Components({ + peerId: peerIdA, + registrar: registrarA + })) + pubsubB = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsubB.init(new Components({ + peerId: peerIdB, + registrar: registrarB + })) + }) + + // start pubsub and connect nodes + beforeEach(async () => { + await Promise.all([ + pubsubA.start(), + pubsubB.start() + ]) + const topologyA = registrarA.getTopologies(protocol)[0] + const handlerB = registrarB.getHandler(protocol) + + if (topologyA == null || handlerB == null) { + throw new Error(`No handler registered for ${protocol}`) + } + + // Notify peers of connection + const [c0, c1] = ConnectionPair() + + await topologyA.onConnect(peerIdB, c0) + await handlerB(await mockIncomingStreamEvent(protocol, c1, peerIdA)) + }) + + afterEach(async () => { + await Promise.all([ + pubsubA.stop(), + pubsubB.stop() + ]) + }) + + it('should send subscribe message to connected peers', async () => { + sinon.spy(pubsubA, 'send') + sinon.spy(pubsubB, 'processRpcSubOpt') + + pubsubA.subscribe(topic) + + // Should send subscriptions to a peer + // @ts-expect-error .callCount is a added by sinon + expect(pubsubA.send.callCount).to.eql(1) + + // Other peer should receive subscription message + await pWaitFor(() => { + const subscribers = pubsubB.getSubscribers(topic) + + return subscribers.length === 1 + }) + + // @ts-expect-error .callCount is a added by sinon + expect(pubsubB.processRpcSubOpt.callCount).to.eql(1) + }) + }) + }) + + describe('unsubscribe', () => { + describe('basics', () => { + let pubsub: PubsubImplementation + + beforeEach(async () => { + const peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + await pubsub.start() + }) + + afterEach(async () => await pubsub.stop()) + + it('should remove all subscriptions for a topic', () => { + pubsub.subscribe(topic) + pubsub.subscribe(topic) + + expect(pubsub.subscriptions.size).to.eql(1) + + pubsub.unsubscribe(topic) + + expect(pubsub.subscriptions.size).to.eql(0) + }) + }) + + describe('two nodes', () => { + let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation + let peerIdA: PeerId, peerIdB: PeerId + let registrarA: MockRegistrar + let registrarB: MockRegistrar + + beforeEach(async () => { + peerIdA = await createPeerId() + peerIdB = await createPeerId() + + registrarA = new MockRegistrar() + registrarB = new MockRegistrar() + + pubsubA = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsubA.init(new Components({ + peerId: peerIdA, + registrar: registrarA + })) + pubsubB = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsubB.init(new Components({ + peerId: peerIdB, + registrar: registrarB + })) + }) + + // start pubsub and connect nodes + beforeEach(async () => { + await Promise.all([ + pubsubA.start(), + pubsubB.start() + ]) + + const topologyA = registrarA.getTopologies(protocol)[0] + const handlerB = registrarB.getHandler(protocol) + + if (topologyA == null || handlerB == null) { + throw new Error(`No handler registered for ${protocol}`) + } + + // Notify peers of connection + const [c0, c1] = ConnectionPair() + + await topologyA.onConnect(peerIdB, c0) + await handlerB(await mockIncomingStreamEvent(protocol, c1, peerIdA)) + }) + + afterEach(async () => { + await Promise.all([ + pubsubA.stop(), + pubsubB.stop() + ]) + }) + + it('should send unsubscribe message to connected peers', async () => { + const pubsubASendSpy = sinon.spy(pubsubA, 'send') + const pubsubBProcessRpcSubOptSpy = sinon.spy(pubsubB, 'processRpcSubOpt') + + pubsubA.subscribe(topic) + // Should send subscriptions to a peer + expect(pubsubASendSpy.callCount).to.eql(1) + + // Other peer should receive subscription message + await pWaitFor(() => { + const subscribers = pubsubB.getSubscribers(topic) + + return subscribers.length === 1 + }) + + expect(pubsubBProcessRpcSubOptSpy.callCount).to.eql(1) + + // Unsubscribe + pubsubA.unsubscribe(topic) + + // Should send subscriptions to a peer + expect(pubsubASendSpy.callCount).to.eql(2) + + // Other peer should receive subscription message + await pWaitFor(() => { + const subscribers = pubsubB.getSubscribers(topic) + + return subscribers.length === 0 + }) + + // @ts-expect-error .callCount is a property added by sinon + expect(pubsubB.processRpcSubOpt.callCount).to.eql(2) + }) + + it('should not send unsubscribe message to connected peers if not subscribed', () => { + const pubsubASendSpy = sinon.spy(pubsubA, 'send') + + // Unsubscribe + pubsubA.unsubscribe(topic) + + // Should send subscriptions to a peer + expect(pubsubASendSpy.callCount).to.eql(0) + }) + }) + }) + + describe('getTopics', () => { + let peerId: PeerId + let pubsub: PubsubImplementation + + beforeEach(async () => { + peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + await pubsub.start() + }) + + afterEach(async () => await pubsub.stop()) + + it('returns the subscribed topics', () => { + let subsTopics = pubsub.getTopics() + expect(subsTopics).to.have.lengthOf(0) + + pubsub.subscribe(topic) + + subsTopics = pubsub.getTopics() + expect(subsTopics).to.have.lengthOf(1) + expect(subsTopics[0]).to.eql(topic) + }) + }) + + describe('getSubscribers', () => { + let peerId: PeerId + let pubsub: PubsubImplementation + + beforeEach(async () => { + peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + }) + + afterEach(async () => await pubsub.stop()) + + it('should fail if pubsub is not started', () => { + const topic = 'test-topic' + + try { + pubsub.getSubscribers(topic) + } catch (err: any) { + expect(err).to.exist() + expect(err.code).to.eql('ERR_NOT_STARTED_YET') + return + } + throw new Error('should fail if pubsub is not started') + }) + + it('should fail if no topic is provided', async () => { + // start pubsub + await pubsub.start() + + try { + // @ts-expect-error invalid params + pubsub.getSubscribers() + } catch (err: any) { + expect(err).to.exist() + expect(err.code).to.eql('ERR_NOT_VALID_TOPIC') + return + } + throw new Error('should fail if no topic is provided') + }) + + it('should get peer subscribed to one topic', async () => { + const topic = 'test-topic' + + // start pubsub + await pubsub.start() + + let peersSubscribed = pubsub.getSubscribers(topic) + expect(peersSubscribed).to.be.empty() + + // Set mock peer subscribed + const peer = new PeerStreams({ id: peerId, protocol: 'a-protocol' }) + const id = peer.id + + const set = new PeerSet() + set.add(id) + + pubsub.topics.set(topic, set) + pubsub.peers.set(peer.id, peer) + + peersSubscribed = pubsub.getSubscribers(topic) + + expect(peersSubscribed).to.not.be.empty() + expect(id.equals(peersSubscribed[0])).to.be.true() + }) + }) + + describe('verification', () => { + let peerId: PeerId + let pubsub: PubsubImplementation + const data = uint8ArrayFromString('bar') + + beforeEach(async () => { + peerId = await createPeerId() + pubsub = new PubsubImplementation({ + multicodecs: [protocol] + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + await pubsub.start() + }) + + afterEach(async () => await pubsub.stop()) + + it('should drop unsigned messages', async () => { + const publishSpy = sinon.spy(pubsub, 'publishMessage') + sinon.spy(pubsub, 'validate') + + const peerStream = new PeerStreams({ + id: await createEd25519PeerId(), + protocol: 'test' + }) + const rpc: PubSubRPC = { + subscriptions: [], + messages: [{ + from: peerStream.id.toBytes(), + data, + sequenceNumber: await noSignMsgId(data), + topic: topic + }] + } + + pubsub.subscribe(topic) + + await pubsub.processRpc(peerStream.id, peerStream, rpc) + + // message should not be delivered + await delay(1000) + + expect(publishSpy).to.have.property('called', false) + }) + + it('should not drop unsigned messages if strict signing is disabled', async () => { + pubsub.globalSignaturePolicy = 'StrictNoSign' + + const publishSpy = sinon.spy(pubsub, 'publishMessage') + sinon.spy(pubsub, 'validate') + + const peerStream = new PeerStreams({ + id: await createEd25519PeerId(), + protocol: 'test' + }) + + const rpc: PubSubRPC = { + subscriptions: [], + messages: [{ + from: peerStream.id.toBytes(), + data, + topic + }] + } + + pubsub.subscribe(topic) + + const deferred = pDefer() + + pubsub.addEventListener('message', (evt) => { + if (evt.detail.topic === topic) { + deferred.resolve() + } + }) + + await pubsub.processRpc(peerStream.id, peerStream, rpc) + + // await message delivery + await deferred.promise + + expect(pubsub.validate).to.have.property('callCount', 1) + expect(publishSpy).to.have.property('callCount', 1) + }) + }) +}) diff --git a/test/sign.spec.js b/test/sign.spec.js deleted file mode 100644 index 614dc02a54..0000000000 --- a/test/sign.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const uint8ArrayConcat = require('uint8arrays/concat') -const uint8ArrayFromString = require('uint8arrays/from-string') - -const { Message } = require('../src/message') -const { - signMessage, - SignPrefix, - verifySignature -} = require('../src/message/sign') -const PeerId = require('peer-id') -const { randomSeqno } = require('../src/utils') - -describe('message signing', () => { - let peerId - before(async () => { - peerId = await PeerId.create({ - bits: 1024 - }) - }) - - it('should be able to sign and verify a message', async () => { - const message = { - from: peerId.id, - data: uint8ArrayFromString('hello'), - seqno: randomSeqno(), - topicIDs: ['test-topic'] - } - - const bytesToSign = uint8ArrayConcat([SignPrefix, Message.encode(message)]) - const expectedSignature = await peerId.privKey.sign(bytesToSign) - - const signedMessage = await signMessage(peerId, message) - - // Check the signature and public key - expect(signedMessage.signature).to.eql(expectedSignature) - expect(signedMessage.key).to.eql(peerId.pubKey.bytes) - - // Verify the signature - const verified = await verifySignature(signedMessage) - expect(verified).to.eql(true) - }) - - it('should be able to extract the public key from an inlined key', async () => { - const secPeerId = await PeerId.create({ keyType: 'secp256k1', bits: 256 }) - - const message = { - from: secPeerId.id, - data: uint8ArrayFromString('hello'), - seqno: randomSeqno(), - topicIDs: ['test-topic'] - } - - const bytesToSign = uint8ArrayConcat([SignPrefix, Message.encode(message)]) - const expectedSignature = await secPeerId.privKey.sign(bytesToSign) - - const signedMessage = await signMessage(secPeerId, message) - - // Check the signature and public key - expect(signedMessage.signature).to.eql(expectedSignature) - signedMessage.key = undefined - - // Verify the signature - const verified = await verifySignature(signedMessage) - expect(verified).to.eql(true) - }) - - it('should be able to extract the public key from the message', async () => { - const message = { - from: peerId.id, - data: uint8ArrayFromString('hello'), - seqno: randomSeqno(), - topicIDs: ['test-topic'] - } - - const bytesToSign = uint8ArrayConcat([SignPrefix, Message.encode(message)]) - const expectedSignature = await peerId.privKey.sign(bytesToSign) - - const signedMessage = await signMessage(peerId, message) - - // Check the signature and public key - expect(signedMessage.signature).to.eql(expectedSignature) - expect(signedMessage.key).to.eql(peerId.pubKey.bytes) - - // Verify the signature - const verified = await verifySignature(signedMessage) - expect(verified).to.eql(true) - }) -}) diff --git a/test/sign.spec.ts b/test/sign.spec.ts new file mode 100644 index 0000000000..94e7e1959c --- /dev/null +++ b/test/sign.spec.ts @@ -0,0 +1,123 @@ +import { expect } from 'aegir/chai' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { RPC } from './message/rpc.js' +import { + signMessage, + SignPrefix, + verifySignature +} from '../src/sign.js' +import * as PeerIdFactory from '@libp2p/peer-id-factory' +import { randomSeqno, toRpcMessage } from '../src/utils.js' +import { keys } from '@libp2p/crypto' +import type { Message, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' +import type { PeerId } from '@libp2p/interfaces/peer-id' + +function encodeMessage (message: PubSubRPCMessage) { + return RPC.Message.encode(message) +} + +describe('message signing', () => { + let peerId: PeerId + + before(async () => { + peerId = await PeerIdFactory.createRSAPeerId({ + bits: 1024 + }) + }) + + it('should be able to sign and verify a message', async () => { + const message: Message = { + from: peerId, + data: uint8ArrayFromString('hello'), + sequenceNumber: randomSeqno(), + topic: 'test-topic' + } + + const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(message))]) + + if (peerId.privateKey == null) { + throw new Error('No private key found on PeerId') + } + + const privateKey = await keys.unmarshalPrivateKey(peerId.privateKey) + const expectedSignature = await privateKey.sign(bytesToSign) + + const signedMessage = await signMessage(peerId, message, encodeMessage) + + // Check the signature and public key + expect(signedMessage.signature).to.equalBytes(expectedSignature) + expect(signedMessage.key).to.equalBytes(peerId.publicKey) + + // Verify the signature + const verified = await verifySignature({ + ...signedMessage, + from: peerId + }, encodeMessage) + expect(verified).to.eql(true) + }) + + it('should be able to extract the public key from an inlined key', async () => { + const secPeerId = await PeerIdFactory.createSecp256k1PeerId() + + const message: Message = { + from: secPeerId, + data: uint8ArrayFromString('hello'), + sequenceNumber: randomSeqno(), + topic: 'test-topic' + } + + const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(message))]) + + if (secPeerId.privateKey == null) { + throw new Error('No private key found on PeerId') + } + + const privateKey = await keys.unmarshalPrivateKey(secPeerId.privateKey) + const expectedSignature = await privateKey.sign(bytesToSign) + + const signedMessage = await signMessage(secPeerId, message, encodeMessage) + + // Check the signature and public key + expect(signedMessage.signature).to.eql(expectedSignature) + signedMessage.key = undefined + + // Verify the signature + const verified = await verifySignature({ + ...signedMessage, + from: secPeerId + }, encodeMessage) + expect(verified).to.eql(true) + }) + + it('should be able to extract the public key from the message', async () => { + const message: Message = { + from: peerId, + data: uint8ArrayFromString('hello'), + sequenceNumber: randomSeqno(), + topic: 'test-topic' + } + + const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(message))]) + + if (peerId.privateKey == null) { + throw new Error('No private key found on PeerId') + } + + const privateKey = await keys.unmarshalPrivateKey(peerId.privateKey) + const expectedSignature = await privateKey.sign(bytesToSign) + + const signedMessage = await signMessage(peerId, message, encodeMessage) + + // Check the signature and public key + expect(signedMessage.signature).to.equalBytes(expectedSignature) + expect(signedMessage.key).to.equalBytes(peerId.publicKey) + + // Verify the signature + const verified = await verifySignature({ + ...signedMessage, + from: peerId + }, encodeMessage) + expect(verified).to.eql(true) + }) +}) diff --git a/test/topic-validators.spec.ts b/test/topic-validators.spec.ts new file mode 100644 index 0000000000..b48cd2c770 --- /dev/null +++ b/test/topic-validators.spec.ts @@ -0,0 +1,110 @@ +import { expect } from 'aegir/chai' +import sinon from 'sinon' +import pWaitFor from 'p-wait-for' +import errCode from 'err-code' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PeerStreams } from '../src/peer-streams.js' +import { + MockRegistrar, + PubsubImplementation +} from './utils/index.js' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { PubSubRPC } from '@libp2p/interfaces/pubsub' +import { Components } from '@libp2p/interfaces/components' + +const protocol = '/pubsub/1.0.0' + +describe('topic validators', () => { + let pubsub: PubsubImplementation + let peerId: PeerId + let otherPeerId: PeerId + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + globalSignaturePolicy: 'StrictNoSign' + }) + pubsub.init(new Components({ + peerId: peerId, + registrar: new MockRegistrar() + })) + + await pubsub.start() + }) + + afterEach(() => { + sinon.restore() + }) + + it('should filter messages by topic validator', async () => { + // use publishMessage.callCount() to see if a message is valid or not + sinon.spy(pubsub, 'publishMessage') + // @ts-expect-error not all fields are implemented in return value + sinon.stub(pubsub.peers, 'get').returns({}) + const filteredTopic = 't' + const peer = new PeerStreams({ id: otherPeerId, protocol: 'a-protocol' }) + + // Set a trivial topic validator + pubsub.topicValidators.set(filteredTopic, async (topic, message) => { + if (!uint8ArrayEquals(message.data, uint8ArrayFromString('a message'))) { + throw errCode(new Error(), 'ERR_TOPIC_VALIDATOR_REJECT') + } + }) + + // valid case + const validRpc: PubSubRPC = { + subscriptions: [], + messages: [{ + from: otherPeerId.multihash.bytes, + data: uint8ArrayFromString('a message'), + topic: filteredTopic + }] + } + + // process valid message + pubsub.subscribe(filteredTopic) + void pubsub.processRpc(peer.id, peer, validRpc) + + // @ts-expect-error .callCount is a property added by sinon + await pWaitFor(() => pubsub.publishMessage.callCount === 1) + + // invalid case + const invalidRpc = { + subscriptions: [], + messages: [{ + data: uint8ArrayFromString('a different message'), + topic: filteredTopic + }] + } + + void pubsub.processRpc(peer.id, peer, invalidRpc) + + // @ts-expect-error .callCount is a property added by sinon + expect(pubsub.publishMessage.callCount).to.eql(1) + + // remove topic validator + pubsub.topicValidators.delete(filteredTopic) + + // another invalid case + const invalidRpc2: PubSubRPC = { + subscriptions: [], + messages: [{ + from: otherPeerId.multihash.bytes, + data: uint8ArrayFromString('a different message'), + topic: filteredTopic + }] + } + + // process previously invalid message, now is valid + void pubsub.processRpc(peer.id, peer, invalidRpc2) + pubsub.unsubscribe(filteredTopic) + + // @ts-expect-error .callCount is a property added by sinon + await pWaitFor(() => pubsub.publishMessage.callCount === 2) + }) +}) diff --git a/test/utils.spec.js b/test/utils.spec.js deleted file mode 100644 index 5a3bd27ca2..0000000000 --- a/test/utils.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('chai') -const utils = require('../src/utils') -const uint8ArrayFromString = require('uint8arrays/from-string') - -describe('utils', () => { - it('randomSeqno', () => { - const first = utils.randomSeqno() - const second = utils.randomSeqno() - - expect(first).to.have.length(8) - expect(second).to.have.length(8) - expect(first).to.not.eql(second) - }) - - it('msgId', () => { - expect(utils.msgId('hello', uint8ArrayFromString('world'))).to.be.eql('hello776f726c64') - }) - - it('msgId should not generate same ID for two different Uint8Arrays', () => { - const peerId = 'QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22' - const msgId0 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfde', 'base16')) - const msgId1 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfe0', 'base16')) - expect(msgId0).to.not.eql(msgId1) - }) - - it('anyMatch', () => { - [ - [[1, 2, 3], [4, 5, 6], false], - [[1, 2], [1, 2], true], - [[1, 2, 3], [4, 5, 1], true], - [[5, 6, 1], [1, 2, 3], true], - [[], [], false], - [[1], [2], false] - ].forEach((test) => { - expect(utils.anyMatch(new Set(test[0]), new Set(test[1]))) - .to.eql(test[2]) - - expect(utils.anyMatch(new Set(test[0]), test[1])) - .to.eql(test[2]) - }) - }) - - it('ensureArray', () => { - expect(utils.ensureArray('hello')).to.be.eql(['hello']) - expect(utils.ensureArray([1, 2])).to.be.eql([1, 2]) - }) - - it('converts an IN msg.from to b58', () => { - const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') - const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' - const m = [ - { from: binaryId }, - { from: stringId } - ] - const expected = [ - { from: stringId }, - { from: stringId } - ] - expect(utils.normalizeInRpcMessages(m)).to.deep.eql(expected) - }) - - it('converts an OUT msg.from to binary', () => { - const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') - const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' - const m = [ - { from: binaryId }, - { from: stringId } - ] - const expected = [ - { from: binaryId }, - { from: binaryId } - ] - expect(utils.normalizeOutRpcMessages(m)).to.deep.eql(expected) - }) -}) diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 0000000000..874f1128d9 --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,89 @@ +import { expect } from 'aegir/chai' +import * as utils from '../src/utils.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { Message, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' +import { peerIdFromBytes, peerIdFromString } from '@libp2p/peer-id' + +describe('utils', () => { + it('randomSeqno', () => { + const first = utils.randomSeqno() + const second = utils.randomSeqno() + + expect(first).to.be.a('BigInt') + expect(second).to.be.a('BigInt') + expect(first).to.not.equal(second) + }) + + it('msgId should not generate same ID for two different Uint8Arrays', () => { + const peerId = peerIdFromString('QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22') + const msgId0 = utils.msgId(peerId.multihash.bytes, 1n) + const msgId1 = utils.msgId(peerId.multihash.bytes, 2n) + expect(msgId0).to.not.deep.equal(msgId1) + }) + + it('anyMatch', () => { + [ + { a: [1, 2, 3], b: [4, 5, 6], result: false }, + { a: [1, 2], b: [1, 2], result: true }, + { a: [1, 2, 3], b: [4, 5, 1], result: true }, + { a: [5, 6, 1], b: [1, 2, 3], result: true }, + { a: [], b: [], result: false }, + { a: [1], b: [2], result: false } + ].forEach((test) => { + expect(utils.anyMatch(new Set(test.a), new Set(test.b))).to.equal(test.result) + expect(utils.anyMatch(new Set(test.a), test.b)).to.equal(test.result) + }) + }) + + it('ensureArray', () => { + expect(utils.ensureArray('hello')).to.be.eql(['hello']) + expect(utils.ensureArray([1, 2])).to.be.eql([1, 2]) + }) + + it('converts an OUT msg.from to binary', () => { + const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') + const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' + const m: Message[] = [{ + from: peerIdFromBytes(binaryId), + topic: '', + data: new Uint8Array() + }, { + from: peerIdFromString(stringId), + topic: '', + data: new Uint8Array() + }] + const expected: PubSubRPCMessage[] = [{ + from: binaryId, + topic: '', + data: new Uint8Array(), + sequenceNumber: undefined, + signature: undefined, + key: undefined + }, { + from: binaryId, + topic: '', + data: new Uint8Array(), + sequenceNumber: undefined, + signature: undefined, + key: undefined + }] + for (let i = 0; i < m.length; i++) { + expect(utils.toRpcMessage(m[i])).to.deep.equal(expected[i]) + } + }) + + it('converts non-negative BigInts to bytes and back', () => { + expect(utils.bigIntFromBytes(utils.bigIntToBytes(1n))).to.equal(1n) + + const values = [ + 0n, + 1n, + 100n, + 192832190818383818719287373223131n + ] + + values.forEach(val => { + expect(utils.bigIntFromBytes(utils.bigIntToBytes(val))).to.equal(val) + }) + }) +}) diff --git a/test/utils/index.js b/test/utils/index.js deleted file mode 100644 index 3279798467..0000000000 --- a/test/utils/index.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' - -const lp = require('it-length-prefixed') -const pipe = require('it-pipe') -const DuplexPair = require('it-pair/duplex') - -const PeerId = require('peer-id') - -const PubsubBaseProtocol = require('../../src') -const { message } = require('../../src') - -exports.createPeerId = async () => { - const peerId = await PeerId.create({ bits: 1024 }) - - return peerId -} - -class PubsubImplementation extends PubsubBaseProtocol { - constructor (protocol, peerId, registrar) { - super({ - debugName: 'libp2p:pubsub', - multicodecs: protocol, - peerId: peerId, - registrar: registrar - }) - } - - publish (topics, messages) { - // ... - } - - subscribe (topics) { - // ... - } - - unsubscribe (topics) { - // ... - } - - _processMessages (idB58Str, conn, peer) { - pipe( - conn, - lp.decode(), - async function (source) { - for await (const val of source) { - const rpc = message.rpc.RPC.decode(val) - - return rpc - } - } - ) - } -} - -exports.PubsubImplementation = PubsubImplementation - -exports.mockRegistrar = { - handle: () => {}, - register: () => {}, - unregister: () => {} -} - -exports.createMockRegistrar = (registrarRecord) => ({ - handle: (multicodecs, handler) => { - const rec = registrarRecord[multicodecs[0]] || {} - - registrarRecord[multicodecs[0]] = { - ...rec, - handler - } - }, - register: ({ multicodecs, _onConnect, _onDisconnect }) => { - const rec = registrarRecord[multicodecs[0]] || {} - - registrarRecord[multicodecs[0]] = { - ...rec, - onConnect: _onConnect, - onDisconnect: _onDisconnect - } - - return multicodecs[0] - }, - unregister: (id) => { - delete registrarRecord[id] - } -}) - -exports.ConnectionPair = () => { - const [d0, d1] = DuplexPair() - - return [ - { - stream: d0, - newStream: () => Promise.resolve({ stream: d0 }) - }, - { - stream: d1, - newStream: () => Promise.resolve({ stream: d1 }) - } - ] -} diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 0000000000..9c6ea11e54 --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1,158 @@ +import { duplexPair } from 'it-pair/duplex' +import * as PeerIdFactory from '@libp2p/peer-id-factory' +import { PubSubBaseProtocol } from '../../src/index.js' +import { RPC } from '../message/rpc.js' +import type { IncomingStreamData, Registrar, StreamHandler } from '@libp2p/interfaces/registrar' +import type { Topology } from '@libp2p/interfaces/topology' +import type { Connection } from '@libp2p/interfaces/connection' +import type { PeerId } from '@libp2p/interfaces/peer-id' +import type { PubSubRPC, PubSubRPCMessage } from '@libp2p/interfaces/pubsub' + +export const createPeerId = async (): Promise => { + const peerId = await PeerIdFactory.createEd25519PeerId() + + return peerId +} + +export class PubsubImplementation extends PubSubBaseProtocol { + async publishMessage () { + return { + recipients: [] + } + } + + decodeRpc (bytes: Uint8Array): PubSubRPC { + return RPC.decode(bytes) + } + + encodeRpc (rpc: PubSubRPC): Uint8Array { + return RPC.encode(rpc) + } + + decodeMessage (bytes: Uint8Array): PubSubRPCMessage { + return RPC.Message.decode(bytes) + } + + encodeMessage (rpc: PubSubRPCMessage): Uint8Array { + return RPC.Message.encode(rpc) + } +} + +export class MockRegistrar implements Registrar { + private readonly topologies: Map = new Map() + private readonly handlers: Map = new Map() + + getProtocols () { + const protocols = new Set() + + for (const topology of this.topologies.values()) { + topology.protocols.forEach(protocol => protocols.add(protocol)) + } + + for (const protocol of this.handlers.keys()) { + protocols.add(protocol) + } + + return Array.from(protocols).sort() + } + + async handle (protocols: string | string[], handler: StreamHandler): Promise { + const protocolList = Array.isArray(protocols) ? protocols : [protocols] + + for (const protocol of protocolList) { + if (this.handlers.has(protocol)) { + throw new Error(`Handler already registered for protocol ${protocol}`) + } + + this.handlers.set(protocol, handler) + } + } + + async unhandle (protocols: string | string[]) { + const protocolList = Array.isArray(protocols) ? protocols : [protocols] + + protocolList.forEach(protocol => { + this.handlers.delete(protocol) + }) + } + + getHandler (protocol: string) { + const handler = this.handlers.get(protocol) + + if (handler == null) { + throw new Error(`No handler registered for protocol ${protocol}`) + } + + return handler + } + + async register (protocols: string | string[], topology: Topology) { + if (!Array.isArray(protocols)) { + protocols = [protocols] + } + + const id = `topology-id-${Math.random()}` + + this.topologies.set(id, { + topology, + protocols + }) + + return id + } + + unregister (id: string | string[]) { + if (!Array.isArray(id)) { + id = [id] + } + + id.forEach(id => this.topologies.delete(id)) + } + + getTopologies (protocol: string) { + const output: Topology[] = [] + + for (const { topology, protocols } of this.topologies.values()) { + if (protocols.includes(protocol)) { + output.push(topology) + } + } + + if (output.length > 0) { + return output + } + + throw new Error(`No topologies registered for protocol ${protocol}`) + } +} + +export const ConnectionPair = (): [Connection, Connection] => { + const [d0, d1] = duplexPair() + + return [ + { + // @ts-expect-error incomplete implementation + newStream: async (protocol: string[]) => await Promise.resolve({ + protocol: protocol[0], + stream: d0 + }) + }, + { + // @ts-expect-error incomplete implementation + newStream: async (protocol: string[]) => await Promise.resolve({ + protocol: protocol[0], + stream: d1 + }) + } + ] +} + +export async function mockIncomingStreamEvent (protocol: string, conn: Connection, remotePeer: PeerId): Promise { + return { + ...await conn.newStream([protocol]), + // @ts-expect-error incomplete implementation + connection: { + remotePeer + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..786aa50305 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "exclude": [ + "test/message/rpc.js" + ] +}