Compare commits

...

110 Commits

Author SHA1 Message Date
Pavel Murygin
96d4a118db fix types 2021-04-09 18:49:09 +03:00
Pavel Murygin
d14c37d14b fix tests 2021-04-09 14:18:10 +03:00
Pavel Murygin
9340ec9d98 fix import issue 2021-04-09 14:12:44 +03:00
Pavel Murygin
d9224d5409 bump dependencies 2021-04-09 13:34:14 +03:00
Vasco Santos
edb8ca60e9 chore: release version v0.9.0 2021-04-07 09:44:27 +02:00
Vasco Santos
810642d8ff chore: update contributors 2021-04-07 09:44:26 +02:00
Vasco Santos
64a478d313
chore: update types (#87) 2021-04-07 09:39:48 +02:00
Vasco Santos
b948972c5b chore: release version v0.8.4 2021-03-22 10:03:40 +01:00
Vasco Santos
edcedf093d chore: update contributors 2021-03-22 10:03:39 +01:00
Alex Potsides
3b960d516f
fix: specify connection direction (#86)
The comment says it can be 'inbound' or 'outbound' so constrain the type to those values.
2021-03-22 09:58:10 +01:00
Vasco Santos
2429e7246f chore: release version v0.8.3 2021-01-26 09:24:52 +01:00
Vasco Santos
22a7f6a9f1 chore: update contributors 2021-01-26 09:24:52 +01:00
Vasco Santos
fb9fce8713
chore: update deps (#82)
* chore: update deps

* chore: add prepare script
2021-01-26 09:11:00 +01:00
Vasco Santos
91dba97125 chore: release version v0.8.2 2021-01-20 10:19:40 +01:00
Vasco Santos
93d712bb0e chore: update contributors 2021-01-20 10:19:40 +01:00
Vasco Santos
ca520775eb
fix: event emitter types with local types (#80) 2021-01-20 10:15:40 +01:00
Vasco Santos
ceb91c672e
chore: add github actions badge (#78) 2020-12-17 11:31:12 +01:00
Vasco Santos
30ffad42c8 chore: release version v0.8.1 2020-12-11 10:06:07 +01:00
Vasco Santos
c365399e4f chore: update contributors 2020-12-11 10:06:07 +01:00
Vasco Santos
5b99e6b56b
fix: pubsub publish message should be uint8array (#77) 2020-12-11 10:01:39 +01:00
Vasco Santos
f4c19c9ef2 chore: release version v0.8.0 2020-12-10 14:29:13 +01:00
Vasco Santos
c36698f488 chore: update contributors 2020-12-10 14:29:13 +01:00
Vasco Santos
3ecbc3e889
chore: remove old pre release script (#76) 2020-12-10 14:23:35 +01:00
Vasco Santos
e2419ea308
feat: add types (#74) 2020-12-10 14:03:17 +01:00
Vasco Santos
04e93d3f08
chore: add pubsub interface to readme (#72) 2020-11-25 18:57:25 +01:00
Vasco Santos
83d7d52d7e chore: release version v0.7.2 2020-11-11 17:20:34 +01:00
Vasco Santos
1a3ea82776 chore: update contributors 2020-11-11 17:20:34 +01:00
Cayman
ad2dfa42dc
chore: pubsub conformance test updates (#70) 2020-11-11 17:16:49 +01:00
Vasco Santos
b75f2cab48 chore: release version v0.7.1 2020-11-03 22:43:57 +01:00
Vasco Santos
8512997e76 chore: update contributors 2020-11-03 22:43:57 +01:00
Cayman
269a6f5e0a
fix: typescript types (#69) 2020-11-03 22:35:18 +01:00
Vasco Santos
14d09970ca chore: release version v0.7.0 2020-11-03 18:26:50 +01:00
Vasco Santos
c98c58e824 chore: update contributors 2020-11-03 18:26:49 +01:00
Cayman
946b046440
feat: pubsub: add global signature policy (#66)
BREAKING CHANGE:
`signMessages` and `strictSigning` pubsub configuration options replaced
with a `globalSignaturePolicy` option
2020-11-03 18:22:03 +01:00
Jacob Heun
d168c7d531
chore: release version v0.6.0 2020-10-05 16:40:42 +02:00
Jacob Heun
349c1174db
chore: update contributors 2020-10-05 16:40:41 +02:00
Cayman
e14844315b
feat: update pubsub getMsgId return type to Uint8Array (#65)
BREAKING CHANGE:
new getMsgId return type is not backwards compatible with prior `string`
return type.
2020-10-05 16:36:29 +02:00
Vasco Santos
ff3bd10704 chore: release version v0.5.2 2020-09-30 11:27:32 +02:00
Vasco Santos
4ecd7d3c83 chore: update contributors 2020-09-30 11:27:31 +02:00
Marin Petrunić
eacdc246da
chore: add type generation from jsdoc (#64) 2020-09-30 11:21:11 +02:00
Jacob Heun
c43e8e26bd
chore: release version v0.5.1 2020-08-25 13:09:56 +02:00
Jacob Heun
94cf0fa4fc
chore: update contributors 2020-08-25 13:09:56 +02:00
Vasco Santos
ba15a48dd9
feat: interface pubsub (#60)
* feat: interface pubsub

* chore: pubsub router tests

* chore: move pubsub abstractions from gossipsub

* chore: address review

* chore: revamp docs

* chore: add emit self tests to interface

* chore: refactor base tests

* chore: publish should only accept one topic per api call

* chore: normalize msg before emit

* chore: do not reset inbound stream

* chore: apply suggestions from code review

Co-authored-by: Jacob Heun <jacobheun@gmail.com>

* chore: address review

* fix: remove subscribe handler

* chore: remove bits from create peerId

Co-authored-by: Jacob Heun <jacobheun@gmail.com>

* chore: remove delay from topic validators tests

* chore: add event emitter information

* fix: topic validator docs

Co-authored-by: Jacob Heun <jacobheun@gmail.com>
2020-08-25 13:05:58 +02:00
Jacob Heun
c4be5eed4b
chore: release version v0.5.0 2020-08-24 12:23:18 +02:00
Jacob Heun
9e0d23de9c
chore: update contributors 2020-08-24 12:23:18 +02:00
Jacob Heun
4130e7f098
fix: replace remaining Buffer usage with Uint8Array (#62)
* fix: marshal record as Uint8Array

BREAKING CHANGE: records now marshal as Uint8Array instead of Buffer

* fix: refactor remaining Buffer usage to Uint8Array
2020-08-24 12:20:24 +02:00
Vasco Santos
d6376377d3
docs: update discovery tests usage (#61) 2020-08-17 18:10:55 +02:00
Jacob Heun
a60ae091e5
chore: release version v0.4.1 2020-08-11 15:26:28 +02:00
Jacob Heun
4902542e78
chore: update contributors 2020-08-11 15:26:27 +02:00
Alex Potsides
4d5f56d06f
chore: update libp2p-tcp (#59)
Updates libp2p-tcp to a version that depends on the same multiaddr as this module.
2020-08-11 15:22:43 +02:00
Jacob Heun
13aa6cbfa0
chore: release version v0.4.0 2020-08-10 12:36:11 +02:00
Jacob Heun
a8ba13da4b
chore: update contributors 2020-08-10 12:36:11 +02:00
Alex Potsides
75f6777d89
chore: update deps (#57)
* chore: update deps

Pulls in latest peer id with Uint8Arrays

BREAKING CHANGES:

- The peer id dep of this module has replaced node Buffers with Uint8Arrays

* chore: update gh deps
2020-08-10 12:23:45 +02:00
Jacob Heun
71b813ad3b
chore: release version v0.3.2 2020-07-15 12:57:26 +02:00
Jacob Heun
46589ce3d0
chore: update contributors 2020-07-15 12:57:25 +02:00
Jacob Heun
f2a18818f2
chore: update deps (#55)
* chore: update deps

* chore: remove unused peer-info dep
2020-07-15 12:56:37 +02:00
Vasco Santos
1cc943e1b2
feat: record interface (#52)
* feat: record interface

* chore: add readme reference

* chore: address review
2020-07-15 12:29:36 +02:00
Jacob Heun
4adedcc4bf
chore: release version v0.3.1 2020-07-03 15:51:41 +02:00
Jacob Heun
0628d708c4
chore: update contributors 2020-07-03 15:51:41 +02:00
Vasco Santos
e10a1545c8
fix: reconnect should trigger topology on connect if protocol stored (#54)
* fix: reconnect should trigger topology on connect if protocol stored

* chore: address review
2020-07-03 15:48:12 +02:00
Vasco Santos
9fbf9d0331 fix: content and peer routing multiaddrs property (#49) 2020-06-05 16:12:33 +02:00
Jacob Heun
aa996d2054 chore: release version v0.3.0 2020-06-05 16:12:33 +02:00
Jacob Heun
507013a724 chore: update contributors 2020-06-05 16:12:33 +02:00
Vasco Santos
a55c7c454a chore: remove peer-info usage on topology (#42)
* chore: remove peer-info usage on topology

BREAKING CHANGE: topology api now uses peer-id instead of peer-info
2020-06-05 16:12:33 +02:00
Vasco Santos
87e2e89791 chore: update content and peer routing interfaces removing peer-info (#43)
* chore: update content and peer routing interfaces removing peer-info

BREAKING CHANGE: content-routing and peer-routing APIs return an object with relevant properties instead of peer-info
2020-06-05 16:12:33 +02:00
Vasco Santos
5bcfc966f7 chore: discovery should not use once (#45) 2020-06-05 16:12:33 +02:00
Vasco Santos
c8c249de6e chore: apply suggestions from code review
Co-Authored-By: Jacob Heun <jacobheun@gmail.com>
2020-06-05 16:12:33 +02:00
Vasco Santos
5b138ef0a0 chore: add tests for peer-discovery 2020-06-05 16:12:33 +02:00
Vasco Santos
bdd2502ef6 feat: peer-discovery not using peer-info
BREAKING CHANGE: peer-discovery emits object with id and multiaddrs properties
2020-06-05 16:12:33 +02:00
dependabot-preview[bot]
1bef8d5d78
chore(deps-dev): bump aegir from 21.10.2 to 22.0.0 (#50)
Bumps [aegir](https://github.com/ipfs/aegir) from 21.10.2 to 22.0.0.
- [Release notes](https://github.com/ipfs/aegir/releases)
- [Changelog](https://github.com/ipfs/aegir/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ipfs/aegir/compare/v21.10.2...v22.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-08 10:52:10 +02:00
kumavis
9a8f375d40
fix: peer-routing typo (#47) 2020-04-22 15:30:59 +02:00
Jacob Heun
afc2aa6891
chore: release version v0.2.8 2020-04-21 11:33:14 +02:00
Jacob Heun
424a0c081e
chore: update contributors 2020-04-21 11:33:14 +02:00
Hugo Dias
e824b15032
chore: update deps (#46) 2020-04-21 11:27:41 +02:00
Vasco Santos
dba3b9932f
Merge pull request #44 from wemeetagain/patch-1
chore: add discv5 peer discovery module
2020-04-11 11:02:12 +02:00
Cayman
c56dd3e3fa
chore: add discv5 peer discovery module 2020-04-10 14:03:58 -05:00
Jacob Heun
de64a49007
chore: release version v0.2.7 2020-03-20 20:57:31 +01:00
Jacob Heun
3cfd9714bd
chore: update contributors 2020-03-20 20:57:30 +01:00
Hugo Dias
78e015cef3
fix: add buffer (#39) 2020-03-20 20:53:54 +01:00
dependabot-preview[bot]
bd5a8b9337 chore(deps): bump sinon from 8.1.1 to 9.0.0
Bumps [sinon](https://github.com/sinonjs/sinon) from 8.1.1 to 9.0.0.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v8.1.1...v9.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-19 08:20:14 -05:00
Jacob Heun
098390cd31
chore: release version v0.2.6 2020-02-17 16:21:31 -05:00
Jacob Heun
953e289e1a
chore: update contributors 2020-02-17 16:21:30 -05:00
dependabot-preview[bot]
ba822856ef chore(deps-dev): bump aegir from 20.6.1 to 21.0.2
Bumps [aegir](https://github.com/ipfs/aegir) from 20.6.1 to 21.0.2.
- [Release notes](https://github.com/ipfs/aegir/releases)
- [Changelog](https://github.com/ipfs/aegir/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ipfs/aegir/compare/v20.6.1...v21.0.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-17 22:19:35 +01:00
Alex Potsides
c77d8de2e7
fix: remove use of assert module (#34)
* fix: remove use of assert module

The polyfill is big, we can simulate it by throwing an Error and it doesn't work under React Native.

* chore: pr comments
2020-02-17 22:19:13 +01:00
Jacob Heun
6203109751
chore: release version v0.2.5 2020-02-04 18:38:20 +01:00
Jacob Heun
25eeedd20e
chore: update contributors 2020-02-04 18:38:19 +01:00
Jacob Heun
5c88d77aaa
fix(connection): tracks streams properly (#25) 2020-02-04 17:34:56 +01:00
dependabot-preview[bot]
f6871afd76 chore(deps): bump sinon from 7.5.0 to 8.1.1
Bumps [sinon](https://github.com/sinonjs/sinon) from 7.5.0 to 8.1.1.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v7.5.0...v8.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-04 15:55:39 +01:00
dependabot-preview[bot]
734c491bb0
chore(deps): bump abortable-iterator from 2.1.0 to 3.0.0 (#24)
Bumps [abortable-iterator](https://github.com/alanshaw/abortable-iterator) from 2.1.0 to 3.0.0.
- [Release notes](https://github.com/alanshaw/abortable-iterator/releases)
- [Commits](https://github.com/alanshaw/abortable-iterator/compare/v2.1.0...v3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-04 15:55:13 +01:00
Jacob Heun
c43a52d176
chore: release version v0.2.4 2020-02-04 12:29:36 +01:00
Jacob Heun
af1ba5a409
chore: update contributors 2020-02-04 12:29:35 +01:00
Vasco Santos
c5b724ab93
fix: dependencies for tests should not be needed by who requires the tests (#18)
* fix: dependencies for tests should not be needed by who requires the tests

* chore: address review
2020-02-04 10:53:05 +01:00
Alan Shaw
6fb45b775b
chore: remove unused dependency (#22) 2020-02-04 10:51:38 +01:00
Jacob Heun
85c68b5df4
chore: release version v0.2.3 2020-01-21 16:10:16 +01:00
Jacob Heun
27ac0a5670
chore: update contributors 2020-01-21 16:10:16 +01:00
Jacob Heun
2de533e15b
fix(transport): make close listener test more resilient (#21) 2020-01-21 16:06:13 +01:00
Jacob Heun
aacb0e27d0
chore: release version v0.2.2 2020-01-17 15:41:02 +01:00
Jacob Heun
c00f94dfaf
chore: update contributors 2020-01-17 15:41:02 +01:00
Jacob Heun
5967834a2f
fix(connection): dont require remoteAddr on creation (#20) 2020-01-17 15:38:14 +01:00
Jacob Heun
32ee3e18e2 chore: release version v0.2.1 2019-12-27 21:46:43 -07:00
Jacob Heun
557f6cab66 chore: update contributors 2019-12-27 21:46:43 -07:00
Jacob Heun
d98bb23fea
feat: add crypto transmission error (#17)
* feat: add crypto transmission error

* docs: add error to crypto readme
2019-12-27 19:51:10 -08:00
Jacob Heun
6cf3723019
chore: release version v0.2.0 2019-12-20 08:14:50 -08:00
Jacob Heun
01e240420d
chore: update contributors 2019-12-20 08:14:49 -08:00
Vasco Santos
ff0313721c fix: transport should not handle connection if upgradeInbound throws (#16)
* fix: transport should not handle connection if upgradeInbound throws

* chore: address review
2019-12-20 08:01:37 -08:00
Jacob Heun
2803e64969
chore: release version v0.1.7 2019-12-15 16:57:40 +01:00
Jacob Heun
e979bc9d4e
chore: update contributors 2019-12-15 16:57:39 +01:00
Jacob Heun
bdbd58e897
feat: export connection status' (#15)
* feat: export the connection status'

* docs(fix): correct status listing
2019-12-15 16:54:43 +01:00
Vasco Santos
79bfcacb61 chore: add lead maintainer property (#14) 2019-12-12 13:05:12 +01:00
Vasco Santos
148f3c9f43 docs: corrected reference to delegated content routing (#13) 2019-12-04 17:19:16 +01:00
78 changed files with 5134 additions and 262 deletions

53
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: ci
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn
- run: yarn lint
- uses: gozala/typescript-error-reporter-action@v1.0.4
- run: yarn build
- run: yarn aegir dep-check
- uses: ipfs/aegir/actions/bundle-size@master
name: size
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
test-node:
needs: check
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
node: [14, 15]
fail-fast: true
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: yarn
- run: npx nyc --reporter=lcov aegir test -t node -- --bail
- uses: codecov/codecov-action@v1
test-chrome:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn
- run: npx aegir test -t browser -t webworker --bail
test-firefox:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn
- run: npx aegir test -t browser -t webworker --bail -- --browsers FirefoxHeadless

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ docs
node_modules
# Lock files
package-lock.json
yarn.lock

View File

@ -1,40 +0,0 @@
language: node_js
cache: npm
stages:
- check
- test
- cov
node_js:
- '10'
- '12'
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 -t webworker
- stage: test
name: firefox
addons:
firefox: latest
script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless
notifications:
email: false

View File

@ -1,3 +1,335 @@
# [0.9.0](https://github.com/libp2p/js-interfaces/compare/v0.8.4...v0.9.0) (2021-04-07)
## [0.8.4](https://github.com/libp2p/js-interfaces/compare/v0.8.3...v0.8.4) (2021-03-22)
### Bug Fixes
* specify connection direction ([#86](https://github.com/libp2p/js-interfaces/issues/86)) ([3b960d5](https://github.com/libp2p/js-interfaces/commit/3b960d516f70f7e198574a736cb09000ddd7a94c))
## [0.8.3](https://github.com/libp2p/js-interfaces/compare/v0.8.2...v0.8.3) (2021-01-26)
## [0.8.2](https://github.com/libp2p/js-interfaces/compare/v0.8.1...v0.8.2) (2021-01-20)
### Bug Fixes
* event emitter types with local types ([#80](https://github.com/libp2p/js-interfaces/issues/80)) ([ca52077](https://github.com/libp2p/js-interfaces/commit/ca520775eb26f5ed501375fdb24ba698c9a8c8c8))
## [0.8.1](https://github.com/libp2p/js-interfaces/compare/v0.8.0...v0.8.1) (2020-12-11)
### Bug Fixes
* pubsub publish message should be uint8array ([#77](https://github.com/libp2p/js-interfaces/issues/77)) ([5b99e6b](https://github.com/libp2p/js-interfaces/commit/5b99e6b56b10439a82ee88fb4e31fb95c182264f))
# [0.8.0](https://github.com/libp2p/js-interfaces/compare/v0.7.2...v0.8.0) (2020-12-10)
### Features
* add types ([#74](https://github.com/libp2p/js-interfaces/issues/74)) ([e2419ea](https://github.com/libp2p/js-interfaces/commit/e2419ea308b5db38966850ba6349602c93ce3b0e))
<a name="0.7.2"></a>
## [0.7.2](https://github.com/libp2p/js-interfaces/compare/v0.7.1...v0.7.2) (2020-11-11)
<a name="0.7.1"></a>
## [0.7.1](https://github.com/libp2p/js-interfaces/compare/v0.7.0...v0.7.1) (2020-11-03)
### Bug Fixes
* typescript types ([#69](https://github.com/libp2p/js-interfaces/issues/69)) ([269a6f5](https://github.com/libp2p/js-interfaces/commit/269a6f5))
<a name="0.7.0"></a>
# [0.7.0](https://github.com/libp2p/js-interfaces/compare/v0.5.2...v0.7.0) (2020-11-03)
### Features
* pubsub: add global signature policy ([#66](https://github.com/libp2p/js-interfaces/issues/66)) ([946b046](https://github.com/libp2p/js-interfaces/commit/946b046))
* update pubsub getMsgId return type to Uint8Array ([#65](https://github.com/libp2p/js-interfaces/issues/65)) ([e148443](https://github.com/libp2p/js-interfaces/commit/e148443))
### BREAKING CHANGES
* `signMessages` and `strictSigning` pubsub configuration options replaced
with a `globalSignaturePolicy` option
* new getMsgId return type is not backwards compatible with prior `string`
return type.
<a name="0.6.0"></a>
# [0.6.0](https://github.com/libp2p/js-interfaces/compare/v0.5.2...v0.6.0) (2020-10-05)
### Features
* update pubsub getMsgId return type to Uint8Array ([#65](https://github.com/libp2p/js-interfaces/issues/65)) ([e148443](https://github.com/libp2p/js-interfaces/commit/e148443))
### BREAKING CHANGES
* new getMsgId return type is not backwards compatible with prior `string`
return type.
<a name="0.5.2"></a>
## [0.5.2](https://github.com/libp2p/js-interfaces/compare/v0.3.1...v0.5.2) (2020-09-30)
### Bug Fixes
* replace remaining Buffer usage with Uint8Array ([#62](https://github.com/libp2p/js-interfaces/issues/62)) ([4130e7f](https://github.com/libp2p/js-interfaces/commit/4130e7f))
### Chores
* update deps ([#57](https://github.com/libp2p/js-interfaces/issues/57)) ([75f6777](https://github.com/libp2p/js-interfaces/commit/75f6777))
### Features
* interface pubsub ([#60](https://github.com/libp2p/js-interfaces/issues/60)) ([ba15a48](https://github.com/libp2p/js-interfaces/commit/ba15a48))
* record interface ([#52](https://github.com/libp2p/js-interfaces/issues/52)) ([1cc943e](https://github.com/libp2p/js-interfaces/commit/1cc943e))
### BREAKING CHANGES
* records now marshal as Uint8Array instead of Buffer
* fix: refactor remaining Buffer usage to Uint8Array
* - The peer id dep of this module has replaced node Buffers with Uint8Arrays
* chore: update gh deps
<a name="0.5.1"></a>
## [0.5.1](https://github.com/libp2p/js-interfaces/compare/v0.5.0...v0.5.1) (2020-08-25)
### Features
* interface pubsub ([#60](https://github.com/libp2p/js-interfaces/issues/60)) ([ba15a48](https://github.com/libp2p/js-interfaces/commit/ba15a48))
<a name="0.5.0"></a>
# [0.5.0](https://github.com/libp2p/js-interfaces/compare/v0.4.1...v0.5.0) (2020-08-24)
### Bug Fixes
* replace remaining Buffer usage with Uint8Array ([#62](https://github.com/libp2p/js-interfaces/issues/62)) ([4130e7f](https://github.com/libp2p/js-interfaces/commit/4130e7f))
### BREAKING CHANGES
* records now marshal as Uint8Array instead of Buffer
* fix: refactor remaining Buffer usage to Uint8Array
<a name="0.4.1"></a>
## [0.4.1](https://github.com/libp2p/js-interfaces/compare/v0.4.0...v0.4.1) (2020-08-11)
<a name="0.4.0"></a>
# [0.4.0](https://github.com/libp2p/js-interfaces/compare/v0.3.2...v0.4.0) (2020-08-10)
### Chores
* update deps ([#57](https://github.com/libp2p/js-interfaces/issues/57)) ([75f6777](https://github.com/libp2p/js-interfaces/commit/75f6777))
### BREAKING CHANGES
* - The peer id dep of this module has replaced node Buffers with Uint8Arrays
* chore: update gh deps
<a name="0.3.2"></a>
## [0.3.2](https://github.com/libp2p/js-interfaces/compare/v0.3.1...v0.3.2) (2020-07-15)
### Features
* record interface ([#52](https://github.com/libp2p/js-interfaces/issues/52)) ([1cc943e](https://github.com/libp2p/js-interfaces/commit/1cc943e))
<a name="0.3.1"></a>
## [0.3.1](https://github.com/libp2p/js-interfaces/compare/v0.2.8...v0.3.1) (2020-07-03)
### Bug Fixes
* content and peer routing multiaddrs property ([#49](https://github.com/libp2p/js-interfaces/issues/49)) ([9fbf9d0](https://github.com/libp2p/js-interfaces/commit/9fbf9d0))
* peer-routing typo ([#47](https://github.com/libp2p/js-interfaces/issues/47)) ([9a8f375](https://github.com/libp2p/js-interfaces/commit/9a8f375))
* reconnect should trigger topology on connect if protocol stored ([#54](https://github.com/libp2p/js-interfaces/issues/54)) ([e10a154](https://github.com/libp2p/js-interfaces/commit/e10a154))
### Chores
* remove peer-info usage on topology ([#42](https://github.com/libp2p/js-interfaces/issues/42)) ([a55c7c4](https://github.com/libp2p/js-interfaces/commit/a55c7c4))
* update content and peer routing interfaces removing peer-info ([#43](https://github.com/libp2p/js-interfaces/issues/43)) ([87e2e89](https://github.com/libp2p/js-interfaces/commit/87e2e89))
### Features
* peer-discovery not using peer-info ([bdd2502](https://github.com/libp2p/js-interfaces/commit/bdd2502))
### BREAKING CHANGES
* topology api now uses peer-id instead of peer-info
* content-routing and peer-routing APIs return an object with relevant properties instead of peer-info
* peer-discovery emits object with id and multiaddrs properties
<a name="0.3.0"></a>
# [0.3.0](https://github.com/libp2p/js-interfaces/compare/v0.2.8...v0.3.0) (2020-04-21)
### Chores
* remove peer-info usage on topology ([#42](https://github.com/libp2p/js-interfaces/issues/42)) ([79a7843](https://github.com/libp2p/js-interfaces/commit/79a7843))
* update content and peer routing interfaces removing peer-info ([#43](https://github.com/libp2p/js-interfaces/issues/43)) ([d2032e6](https://github.com/libp2p/js-interfaces/commit/d2032e6))
### Features
* peer-discovery not using peer-info ([5792b13](https://github.com/libp2p/js-interfaces/commit/5792b13))
### BREAKING CHANGES
* topology api now uses peer-id instead of peer-info
* content-routing and peer-routing APIs return an object with relevant properties instead of peer-info
* peer-discovery emits object with id and multiaddrs properties
<a name="0.2.8"></a>
## [0.2.8](https://github.com/libp2p/js-interfaces/compare/v0.2.7...v0.2.8) (2020-04-21)
<a name="0.2.7"></a>
## [0.2.7](https://github.com/libp2p/js-interfaces/compare/v0.2.6...v0.2.7) (2020-03-20)
### Bug Fixes
* add buffer ([#39](https://github.com/libp2p/js-interfaces/issues/39)) ([78e015c](https://github.com/libp2p/js-interfaces/commit/78e015c))
<a name="0.2.6"></a>
## [0.2.6](https://github.com/libp2p/js-interfaces/compare/v0.2.5...v0.2.6) (2020-02-17)
### Bug Fixes
* remove use of assert module ([#34](https://github.com/libp2p/js-interfaces/issues/34)) ([c77d8de](https://github.com/libp2p/js-interfaces/commit/c77d8de))
<a name="0.2.5"></a>
## [0.2.5](https://github.com/libp2p/js-interfaces/compare/v0.2.4...v0.2.5) (2020-02-04)
### Bug Fixes
* **connection:** tracks streams properly ([#25](https://github.com/libp2p/js-interfaces/issues/25)) ([5c88d77](https://github.com/libp2p/js-interfaces/commit/5c88d77))
<a name="0.2.4"></a>
## [0.2.4](https://github.com/libp2p/js-interfaces/compare/v0.2.3...v0.2.4) (2020-02-04)
### Bug Fixes
* dependencies for tests should not be needed by who requires the tests ([#18](https://github.com/libp2p/js-interfaces/issues/18)) ([c5b724a](https://github.com/libp2p/js-interfaces/commit/c5b724a))
<a name="0.2.3"></a>
## [0.2.3](https://github.com/libp2p/js-interfaces/compare/v0.2.2...v0.2.3) (2020-01-21)
### Bug Fixes
* **transport:** make close listener test more resilient ([#21](https://github.com/libp2p/js-interfaces/issues/21)) ([2de533e](https://github.com/libp2p/js-interfaces/commit/2de533e))
<a name="0.2.2"></a>
## [0.2.2](https://github.com/libp2p/js-interfaces/compare/v0.2.1...v0.2.2) (2020-01-17)
### Bug Fixes
* **connection:** dont require remoteAddr on creation ([#20](https://github.com/libp2p/js-interfaces/issues/20)) ([5967834](https://github.com/libp2p/js-interfaces/commit/5967834))
<a name="0.2.1"></a>
## [0.2.1](https://github.com/libp2p/js-interfaces/compare/v0.2.0...v0.2.1) (2019-12-28)
### Features
* add crypto transmission error ([#17](https://github.com/libp2p/js-interfaces/issues/17)) ([d98bb23](https://github.com/libp2p/js-interfaces/commit/d98bb23))
<a name="0.2.0"></a>
# [0.2.0](https://github.com/libp2p/js-interfaces/compare/v0.1.7...v0.2.0) (2019-12-20)
### Bug Fixes
* transport should not handle connection if upgradeInbound throws ([#16](https://github.com/libp2p/js-interfaces/issues/16)) ([ff03137](https://github.com/libp2p/js-interfaces/commit/ff03137))
<a name="0.1.7"></a>
## [0.1.7](https://github.com/libp2p/js-interfaces/compare/v0.1.6...v0.1.7) (2019-12-15)
### Features
* export connection status' ([#15](https://github.com/libp2p/js-interfaces/issues/15)) ([bdbd58e](https://github.com/libp2p/js-interfaces/commit/bdbd58e))
<a name="0.1.6"></a>
## [0.1.6](https://github.com/libp2p/js-interfaces/compare/v0.1.5...v0.1.6) (2019-12-02)

View File

@ -4,6 +4,8 @@
[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p)
[![](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io)
[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-interfaces.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-interfaces)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/libp2p/js-libp2p-interfaces/ci?label=ci&style=flat-square)](https://github.com/libp2p/js-libp2p-interfaces/actions?query=branch%3Amaster+workflow%3Aci+)
> Contains test suites and interfaces you can use to implement the various components of libp2p.
@ -18,6 +20,8 @@
- [Crypto](./src/crypto)
- [Peer Discovery](./src/peer-discovery)
- [Peer Routing](./src/peer-routing)
- [Pubsub](./src/pubsub)
- [Record](./src/record)
- [Stream Muxer](./src/stream-muxer)
- [Topology](./src/topology)
- [Transport](./src/transport)
@ -29,6 +33,7 @@ For posterity, here are links to the original repositories for each of the inter
- [Content Routing](https://github.com/libp2p/interface-content-routing)
- [Peer Discovery](https://github.com/libp2p/interface-peer-discovery)
- [Peer Routing](https://github.com/libp2p/interface-peer-routing)
- [Pubsub](https://github.com/libp2p/js-libp2p-pubsub)
- [Stream Muxer](https://github.com/libp2p/interface-stream-muxer)
- [Transport](https://github.com/libp2p/interface-transport)

View File

@ -1,13 +1,28 @@
{
"name": "libp2p-interfaces",
"version": "0.1.6",
"version": "0.9.0",
"description": "Interfaces for JS Libp2p",
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
"main": "src/index.js",
"files": [
"src",
"types",
"dist"
],
"types": "dist/src/index.d.ts",
"typesVersions": {
"*": {
"src/*": [
"dist/src/*",
"dist/src/*/index"
]
}
},
"eslintConfig": {
"extends": "ipfs"
},
"scripts": {
"prepare": "aegir build --no-bundle",
"lint": "aegir lint",
"build": "aegir build",
"test": "aegir test",
@ -32,29 +47,43 @@
},
"homepage": "https://github.com/libp2p/js-interfaces#readme",
"dependencies": {
"@types/bl": "4.1.0",
"abort-controller": "^3.0.0",
"abortable-iterator": "^2.1.0",
"async-iterator-to-pull-stream": "^1.3.0",
"chai": "^4.2.0",
"abortable-iterator": "^3.0.0",
"chai": "^4.3.4",
"chai-checkmark": "^1.0.1",
"class-is": "^1.1.0",
"debug": "^4.3.1",
"delay": "^5.0.0",
"detect-node": "^2.0.4",
"dirty-chai": "^2.0.1",
"err-code": "^2.0.0",
"it-goodbye": "^2.0.1",
"libp2p-tcp": "^0.14.1",
"multiaddr": "^7.1.0",
"p-limit": "^2.2.1",
"peer-id": "^0.13.3",
"sinon": "^7.5.0",
"streaming-iterables": "^4.1.0"
"err-code": "^3.0.1",
"it-goodbye": "^2.0.2",
"it-length-prefixed": "^3.1.0",
"it-pair": "^1.0.0",
"it-pipe": "^1.1.0",
"it-pushable": "^1.4.2",
"libp2p-crypto": "fluencelabs/js-libp2p-crypto",
"libp2p-tcp": "fluencelabs/js-libp2p-tcp",
"multiaddr": "^9.0.1",
"multibase": "^4.0.2",
"multihashes": "^4.0.2",
"p-defer": "^3.0.0",
"p-limit": "^3.1.0",
"p-wait-for": "^3.2.0",
"peer-id": "fluencelabs/js-peer-id",
"protons": "^2.0.0",
"sinon": "^10.0.0",
"streaming-iterables": "^5.0.4",
"uint8arrays": "^2.1.3"
},
"devDependencies": {
"aegir": "^20.4.1",
"it-handshake": "^1.0.0",
"it-pair": "^1.0.0",
"it-pipe": "^1.0.1",
"peer-info": "^0.17.0"
"@types/debug": "^4.1.5",
"aegir": "^32.1.0",
"cids": "^1.1.6",
"events": "^3.3.0",
"it-handshake": "^1.0.2",
"rimraf": "^3.0.2",
"util": "^0.12.3"
},
"contributors": [
"Alan Shaw <alan.shaw@protocol.ai>",
@ -64,6 +93,7 @@
"Friedel Ziegelmayer <dignifiedquire@gmail.com>",
"Greg Zuro <gregzuro@users.noreply.github.com>",
"Jacob Heun <jacobheun@gmail.com>",
"Jacob Heun <jacobheun@Jacobs-MacBook-Pro.local>",
"James Ray <16969914+jamesray1@users.noreply.github.com>",
"Jeffrey Hulten <jhulten@gmail.com>",
"João Santos <joaosantos15@users.noreply.github.com>",
@ -76,6 +106,7 @@
"Sathya Narrayanan <plasmashadowx@gmail.com>",
"Vasco Santos <vasco.santos@moxy.studio>",
"Vasco Santos <vasco.santos@ua.pt>",
"dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>",
"dirkmc <dirkmdev@gmail.com>",
"dmitriy ryajov <dryajov@dmitriys-MBP.HomeNET>",
"greenkeeperio-bot <support@greenkeeper.io>",

View File

@ -110,7 +110,7 @@ const conn = new Connection({
Creates a new Connection instance.
`localAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used by the local peer to reach the remote.
`remoteAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer.
`remoteAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer.
`localPeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the local peer.
`remotePeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the remote peer.
`newStream` is the `function` responsible for getting a new muxed+multistream-selected stream.
@ -121,6 +121,7 @@ Creates a new Connection instance.
- `timeline` is an `object` with the relevant events timestamps of the connection (`open`, `upgraded` and `closed`; the `closed` will be added when the connection is closed).
- `multiplexer` is a `string` with the connection multiplexing codec (optional).
- `encryption` is a `string` with the connection encryption method identifier (optional).
- `status` is a `string` indicating the overall status of the connection. It is one of [`'open'`, `'closing'`, `'closed'`]
#### Create a new stream
@ -220,7 +221,17 @@ This getter returns an `Object` with the metadata of the connection, as follows:
- `status`:
This property contains the status of the connection. It can be either `open`, `closing` or `closed`. Once the connection is created it is in an `open` status. When a `conn.close()` happens, the status will change to `closing` and finally, after all the connection streams are properly closed, the status will be `closed`.
This property contains the status of the connection. It can be either `open`, `closing` or `closed`. Once the connection is created it is in an `open` status. When a `conn.close()` happens, the status will change to `closing` and finally, after all the connection streams are properly closed, the status will be `closed`. These values can also be directly referenced by importing the `status` file:
```js
const {
OPEN, CLOSING, CLOSED
} = require('libp2p-interfaces/src/connection/status')
if (connection.stat.status === OPEN) {
// ...
}
```
- `timeline`:

View File

@ -2,11 +2,42 @@
const PeerId = require('peer-id')
const multiaddr = require('multiaddr')
const withIs = require('class-is')
const assert = require('assert')
const errCode = require('err-code')
const { OPEN, CLOSING, CLOSED } = require('./status')
const connectionSymbol = Symbol.for('@libp2p/interface-connection/connection')
/**
* @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream
* @typedef {import('./status').Status} Status
*/
/**
* @typedef {Object} Timeline
* @property {number} open - connection opening timestamp.
* @property {number} [upgraded] - connection upgraded timestamp.
* @property {number} [close]
*
* @typedef {Object} ConectionStat
* @property {'inbound' | 'outbound'} direction - connection establishment direction
* @property {Timeline} timeline - connection relevant events timestamp.
* @property {string} [multiplexer] - connection multiplexing identifier.
* @property {string} [encryption] - connection encryption method identifier.
*
* @typedef {Object} ConnectionOptions
* @property {multiaddr} [localAddr] - local multiaddr of the connection if known.
* @property {multiaddr} remoteAddr - remote multiaddr of the connection.
* @property {PeerId} localPeer - local peer-id.
* @property {PeerId} remotePeer - remote peer-id.
* @property {(protocols: string|string[]) => Promise<{stream: MuxedStream, protocol: string}>} newStream - new stream muxer function.
* @property {() => Promise<void>} close - close raw connection function.
* @property {() => MuxedStream[]} getStreams - get streams from muxer function.
* @property {ConectionStat} stat - metadata of the connection.
*
* @typedef {Object} StreamData
* @property {string} protocol - the protocol used by the stream
* @property {Object} [metadata] - metadata of the stream
*/
/**
* An implementation of the js-libp2p connection.
@ -14,41 +45,19 @@ const errCode = require('err-code')
*/
class Connection {
/**
* Creates an instance of Connection.
* @param {object} properties properties of the connection.
* @param {multiaddr} [properties.localAddr] local multiaddr of the connection if known.
* @param {multiaddr} properties.remoteAddr remote multiaddr of the connection.
* @param {PeerId} properties.localPeer local peer-id.
* @param {PeerId} properties.remotePeer remote peer-id.
* @param {function} properties.newStream new stream muxer function.
* @param {function} properties.close close raw connection function.
* @param {function} properties.getStreams get streams from muxer function.
* @param {object} properties.stat metadata of the connection.
* @param {string} properties.stat.direction connection establishment direction ("inbound" or "outbound").
* @param {object} properties.stat.timeline connection relevant events timestamp.
* @param {string} properties.stat.timeline.open connection opening timestamp.
* @param {string} properties.stat.timeline.upgraded connection upgraded timestamp.
* @param {string} [properties.stat.multiplexer] connection multiplexing identifier.
* @param {string} [properties.stat.encryption] connection encryption method identifier.
* An implementation of the js-libp2p connection.
* Any libp2p transport should use an upgrader to return this connection.
*
* @class
* @param {ConnectionOptions} options
*/
constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) {
localAddr && assert(multiaddr.isMultiaddr(localAddr), 'localAddr must be an instance of multiaddr')
assert(multiaddr.isMultiaddr(remoteAddr), 'remoteAddr must be an instance of multiaddr')
assert(PeerId.isPeerId(localPeer), 'localPeer must be an instance of peer-id')
assert(PeerId.isPeerId(remotePeer), 'remotePeer must be an instance of peer-id')
assert(typeof newStream === 'function', 'new stream must be a function')
assert(typeof close === 'function', 'close must be a function')
assert(typeof getStreams === 'function', 'getStreams must be a function')
assert(stat, 'connection metadata object must be provided')
assert(stat.direction === 'inbound' || stat.direction === 'outbound', 'direction must be "inbound" or "outbound"')
assert(stat.timeline, 'connection timeline object must be provided in the stat object')
assert(stat.timeline.open, 'connection open timestamp must be provided')
assert(stat.timeline.upgraded, 'connection upgraded timestamp must be provided')
validateArgs(localAddr, localPeer, remotePeer, newStream, close, getStreams, stat)
/**
* Connection identifier.
*/
this.id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now()
this.id = (parseInt(String(Math.random() * 1e9))).toString(36) + Date.now()
/**
* Observed multiaddr of the local peer
@ -72,10 +81,12 @@ class Connection {
/**
* Connection metadata.
*
* @type {ConectionStat & {status: Status}}
*/
this._stat = {
...stat,
status: 'open'
status: OPEN
}
/**
@ -100,13 +111,34 @@ class Connection {
/**
* User provided tags
*
* @type {string[]}
*/
this.tags = []
}
get [Symbol.toStringTag] () {
return 'Connection'
}
get [connectionSymbol] () {
return true
}
/**
* Checks if the given value is a `Connection` instance.
*
* @param {any} other
* @returns {other is Connection}
*/
static isConnection (other) {
return Boolean(other && other[connectionSymbol])
}
/**
* Get connection metadata
* @return {Object}
*
* @this {Connection}
*/
get stat () {
return this._stat
@ -114,7 +146,8 @@ class Connection {
/**
* Get all the streams of the muxer.
* @return {Array<*>}
*
* @this {Connection}
*/
get streams () {
return this._getStreams()
@ -122,15 +155,16 @@ class Connection {
/**
* Create a new stream from this connection
* @param {string[]} protocols intended protocol for the stream
* @return {Promise<object>} with muxed+multistream-selected stream and selected protocol
*
* @param {string|string[]} protocols - intended protocol for the stream
* @returns {Promise<{stream: MuxedStream, protocol: string}>} with muxed+multistream-selected stream and selected protocol
*/
async newStream (protocols) {
if (this.stat.status === 'closing') {
if (this.stat.status === CLOSING) {
throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED')
}
if (this.stat.status === 'closed') {
if (this.stat.status === CLOSED) {
throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED')
}
@ -138,7 +172,7 @@ class Connection {
const { stream, protocol } = await this._newStream(protocols)
this.addStream(stream, protocol)
this.addStream(stream, { protocol })
return {
stream,
@ -148,11 +182,10 @@ class Connection {
/**
* Add a stream when it is opened to the registry.
* @param {*} muxedStream a muxed stream
* @param {object} properties the stream properties to be registered
* @param {string} properties.protocol the protocol used by the stream
* @param {object} properties.metadata metadata of the stream
* @return {void}
*
* @param {MuxedStream} muxedStream - a muxed stream
* @param {StreamData} data - the stream data to be registered
* @returns {void}
*/
addStream (muxedStream, { protocol, metadata = {} }) {
// Add metadata for the stream
@ -164,7 +197,8 @@ class Connection {
/**
* Remove stream registry after it is closed.
* @param {string} id identifier of the stream
*
* @param {string} id - identifier of the stream
*/
removeStream (id) {
this.registry.delete(id)
@ -172,10 +206,11 @@ class Connection {
/**
* Close the connection.
* @return {Promise}
*
* @returns {Promise<void>}
*/
async close () {
if (this.stat.status === 'closed') {
if (this.stat.status === CLOSED) {
return
}
@ -183,14 +218,69 @@ class Connection {
return this._closing
}
this.stat.status = 'closing'
this.stat.status = CLOSING
// Close raw connection
this._closing = await this._close()
this._stat.timeline.close = Date.now()
this.stat.status = 'closed'
this.stat.status = CLOSED
}
}
module.exports = withIs(Connection, { className: 'Connection', symbolName: '@libp2p/interface-connection/connection' })
module.exports = Connection
/**
* @param {multiaddr|undefined} localAddr
* @param {PeerId} localPeer
* @param {PeerId} remotePeer
* @param {(protocols: string | string[]) => Promise<{ stream: import("../stream-muxer/types").MuxedStream; protocol: string; }>} newStream
* @param {() => Promise<void>} close
* @param {() => import("../stream-muxer/types").MuxedStream[]} getStreams
* @param {{ direction: any; timeline: any; multiplexer?: string | undefined; encryption?: string | undefined; }} stat
*/
function validateArgs (localAddr, localPeer, remotePeer, newStream, close, getStreams, stat) {
if (localAddr && !multiaddr.Multiaddr.isMultiaddr(localAddr)) {
throw errCode(new Error('localAddr must be an instance of multiaddr'), 'ERR_INVALID_PARAMETERS')
}
if (!PeerId.isPeerId(localPeer)) {
throw errCode(new Error('localPeer must be an instance of peer-id'), 'ERR_INVALID_PARAMETERS')
}
if (!PeerId.isPeerId(remotePeer)) {
throw errCode(new Error('remotePeer must be an instance of peer-id'), 'ERR_INVALID_PARAMETERS')
}
if (typeof newStream !== 'function') {
throw errCode(new Error('new stream must be a function'), 'ERR_INVALID_PARAMETERS')
}
if (typeof close !== 'function') {
throw errCode(new Error('close must be a function'), 'ERR_INVALID_PARAMETERS')
}
if (typeof getStreams !== 'function') {
throw errCode(new Error('getStreams must be a function'), 'ERR_INVALID_PARAMETERS')
}
if (!stat) {
throw errCode(new Error('connection metadata object must be provided'), 'ERR_INVALID_PARAMETERS')
}
if (stat.direction !== 'inbound' && stat.direction !== 'outbound') {
throw errCode(new Error('direction must be "inbound" or "outbound"'), 'ERR_INVALID_PARAMETERS')
}
if (!stat.timeline) {
throw errCode(new Error('connection timeline object must be provided in the stat object'), 'ERR_INVALID_PARAMETERS')
}
if (!stat.timeline.open) {
throw errCode(new Error('connection open timestamp must be provided'), 'ERR_INVALID_PARAMETERS')
}
if (!stat.timeline.upgraded) {
throw errCode(new Error('connection upgraded timestamp must be provided'), 'ERR_INVALID_PARAMETERS')
}
}

12
src/connection/status.js Normal file
View File

@ -0,0 +1,12 @@
'use strict'
const STATUS = {
OPEN: /** @type {'open'} */('open'),
CLOSING: /** @type {'closing'} */('closing'),
CLOSED: /** @type {'closed'} */('closed')
}
module.exports = STATUS
/**
* @typedef {STATUS[keyof STATUS]} Status
*/

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -6,6 +7,7 @@ const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
const sinon = require('sinon')
const Status = require('../status')
module.exports = (test) => {
describe('connection', () => {
@ -28,7 +30,7 @@ module.exports = (test) => {
expect(connection.remotePeer).to.exist()
expect(connection.localAddr).to.exist()
expect(connection.remoteAddr).to.exist()
expect(connection.stat.status).to.equal('open')
expect(connection.stat.status).to.equal(Status.OPEN)
expect(connection.stat.timeline.open).to.exist()
expect(connection.stat.timeline.upgraded).to.exist()
expect(connection.stat.timeline.close).to.not.exist()
@ -40,7 +42,7 @@ module.exports = (test) => {
it('should get the metadata of an open connection', () => {
const stat = connection.stat
expect(stat.status).to.equal('open')
expect(stat.status).to.equal(Status.OPEN)
expect(stat.direction).to.exist()
expect(stat.timeline.open).to.exist()
expect(stat.timeline.upgraded).to.exist()
@ -73,6 +75,7 @@ module.exports = (test) => {
let timelineProxy
const proxyHandler = {
set () {
// @ts-ignore - TS fails to infer here
return Reflect.set(...arguments)
}
}
@ -103,7 +106,7 @@ module.exports = (test) => {
await connection.close()
expect(connection.stat.timeline.close).to.exist()
expect(connection.stat.status).to.equal('closed')
expect(connection.stat.status).to.equal(Status.CLOSED)
})
it('should be able to close the connection after opening a stream', async () => {
@ -116,7 +119,20 @@ module.exports = (test) => {
await connection.close()
expect(connection.stat.timeline.close).to.exist()
expect(connection.stat.status).to.equal('closed')
expect(connection.stat.status).to.equal(Status.CLOSED)
})
it('should properly track streams', async () => {
// Open stream
const protocol = '/echo/0.0.1'
const { stream } = await connection.newStream(protocol)
const trackedStream = connection.registry.get(stream.id)
expect(trackedStream).to.have.property('protocol', protocol)
// Close stream
await stream.close()
expect(connection.registry.get(stream.id)).to.not.exist()
})
it('should support a proxy on the timeline', async () => {
@ -124,7 +140,9 @@ module.exports = (test) => {
expect(connection.stat.timeline.close).to.not.exist()
await connection.close()
// @ts-ignore - fails to infer callCount
expect(proxyHandler.set.callCount).to.equal(1)
// @ts-ignore - fails to infer getCall
const [obj, key, value] = proxyHandler.set.getCall(0).args
expect(obj).to.eql(connection.stat.timeline)
expect(key).to.equal('close')

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -12,7 +12,7 @@ Publishing a test suite as a module lets multiple modules all ensure compatibili
# Modules that implement the interface
- [JavaScript libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
- [JavaScript libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing)
- [JavaScript libp2p-delegated-content-routing](https://github.com/libp2p/js-libp2p-delegated-content-routing)
# Badge
@ -30,6 +30,32 @@ TBD
A valid (read: that follows this abstraction) Content Routing module must implement the following API.
### `.findProviders`
### findProviders
### `.provide`
- `findProviders(cid)`
Find peers in the network that can provide a specific value, given a key.
**Parameters**
- [CID](https://github.com/multiformats/js-cid)
**Returns**
It returns an `AsyncIterable` containing the identification and addresses of the peers providing the given key, as follows:
`AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>`
### provide
- `provide(cid)`
Announce to the network that we are providing the given value.
**Parameters**
- [CID](https://github.com/multiformats/js-cid)
**Returns**
It returns a promise that is resolved on the success of the operation.
`Promise<void>`

11
src/content-routing/types.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export = ContentRouting;
import PeerId from 'peer-id'
import { Multiaddr } from 'multiaddr'
import CID from 'cids'
declare class ContentRouting {
constructor (options: Object);
provide (cid: CID): Promise<void>;
findProviders (cid: CID, options: Object): AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>;
}

View File

@ -85,6 +85,7 @@ Common crypto errors come with the interface, and can be imported directly. All
```js
const {
InvalidCryptoExchangeError,
InvalidCryptoTransmissionError,
UnexpectedPeerError
} = require('libp2p-interfaces/src/crypto/errors')
@ -95,4 +96,5 @@ console.log(error.code === UnexpectedPeerError.code) // true
### Error Types
- `InvalidCryptoExchangeError` - Should be thrown when a peer provides data that is insufficient to finish the crypto exchange.
- `InvalidCryptoTransmissionError` - Should be thrown when an error occurs during encryption/decryption.
- `UnexpectedPeerError` - Should be thrown when the expected peer id does not match the peer id determined via the crypto exchange.

View File

@ -22,7 +22,19 @@ class InvalidCryptoExchangeError extends Error {
}
}
class InvalidCryptoTransmissionError extends Error {
constructor (message = 'Invalid crypto transmission') {
super(message)
this.code = InvalidCryptoTransmissionError.code
}
static get code () {
return 'ERR_INVALID_CRYPTO_TRANSMISSION'
}
}
module.exports = {
UnexpectedPeerError,
InvalidCryptoExchangeError
InvalidCryptoExchangeError,
InvalidCryptoTransmissionError
}

View File

@ -1,16 +1,20 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const duplexPair = require('it-pair/duplex')
const pipe = require('it-pipe')
const peers = require('../../utils/peers')
const { UnexpectedPeerError } = require('../errors')
const PeerId = require('peer-id')
const { collect } = require('streaming-iterables')
const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
const duplexPair = require('it-pair/duplex')
const { pipe } = require('it-pipe')
const PeerId = require('peer-id')
const { collect } = require('streaming-iterables')
const uint8arrayFromString = require('uint8arrays/from-string')
const peers = require('../../utils/peers')
const { UnexpectedPeerError } = require('../errors')
module.exports = (common) => {
describe('interface-crypto', () => {
let crypto
@ -54,7 +58,7 @@ module.exports = (common) => {
pipe(inboundResult.conn, inboundResult.conn)
// Send some data and collect the result
const input = Buffer.from('data to encrypt')
const input = uint8arrayFromString('data to encrypt')
const result = await pipe(
[input],
outboundResult.conn,

24
src/crypto/types.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import PeerId from 'peer-id'
import { MultiaddrConnection } from '../transport/types'
/**
* A libp2p crypto module must be compliant to this interface
* to ensure all exchanged data between two peers is encrypted.
*/
export interface Crypto {
protocol: string;
/**
* Encrypt outgoing data to the remote party.
*/
secureOutbound(localPeer: PeerId, connection: MultiaddrConnection, remotePeer: PeerId): Promise<SecureOutbound>;
/**
* Decrypt incoming data.
*/
secureInbound(localPeer: PeerId, connection: MultiaddrConnection, remotePeer?: PeerId): Promise<SecureOutbound>;
}
export type SecureOutbound = {
conn: MultiaddrConnection;
remoteEarlyData: Buffer;
remotePeer: PeerId;
}

View File

@ -16,6 +16,7 @@ The API is presented with both Node.js and Go primitives, however, there is not
- [JavaScript libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
- [JavaScript libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star)
- [JavaScript libp2p-websocket-star](https://github.com/libp2p/js-libp2p-websocket-star)
- [TypeScript discv5](https://github.com/chainsafe/discv5)
Send a PR to add a new one if you happen to find or write one.
@ -32,19 +33,19 @@ Include this badge in your readme if you make a new module that uses interface-p
Install `interface-discovery` as one of the dependencies of your project and as a test file. Then, using `mocha` (for JavaScript) or a test runner with compatible API, do:
```js
const test = require('interface-discovery')
const tests = require('libp2p-interfaces/src/peer-discovery/tests')
const common = {
setup () {
return YourDiscovery
},
teardown () {
// Clean up any resources created by setup()
}
}
// use all of the test suits
test(common)
describe('your discovery', () => {
// use all of the test suits
tests({
setup () {
return YourDiscovery
},
teardown () {
// Clean up any resources created by setup()
}
})
})
```
## API
@ -69,6 +70,9 @@ It returns a `Promise`
### discoverying peers
- `discovery.on('peer', (peerInfo) => {})`
- `discovery.on('peer', (peerData) => {})`
Everytime a peer is discovered by a discovery service, it emmits a `peer` event with the discover peer's [PeerInfo](https://github.com/libp2p/js-peer-info).
Every time a peer is discovered by a discovery service, it emits a `peer` event with the discovered peer's information, which must contain the following properties:
- `<`[`PeerId`](https://github.com/libp2p/js-peer-id)`>` `peerData.id`
- `<Array<`[`Multiaddr`](https://github.com/multiformats/js-multiaddr)`>>` `peerData.multiaddrs`

View File

@ -1,17 +1,32 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
const multiaddr = require('multiaddr')
const PeerId = require('peer-id')
const delay = require('delay')
const pDefer = require('p-defer')
module.exports = (common) => {
describe('interface-peer-discovery', () => {
let discovery
before(async () => {
beforeEach(async () => {
discovery = await common.setup()
})
after(() => common.teardown && common.teardown())
afterEach('ensure discovery was stopped', async () => {
await discovery.stop()
afterEach('ensure discovery was stopped', () => discovery.stop())
discovery.removeAllListeners()
common.teardown && common.teardown()
})
it('can start the service', async () => {
await discovery.start()
@ -30,5 +45,49 @@ module.exports = (common) => {
await discovery.start()
await discovery.start()
})
it('should emit a peer event after start', async () => {
const defer = pDefer()
await discovery.start()
discovery.on('peer', ({ id, multiaddrs }) => {
expect(id).to.exist()
expect(PeerId.isPeerId(id)).to.eql(true)
expect(multiaddrs).to.exist()
multiaddrs.forEach((m) => expect(multiaddr.Multiaddr.isMultiaddr(m)).to.eql(true))
defer.resolve()
})
await defer.promise
})
it('should not receive a peer event before start', async () => {
discovery.on('peer', () => {
throw new Error('should not receive a peer event before start')
})
await delay(2000)
})
it('should not receive a peer event after stop', async () => {
const deferStart = pDefer()
await discovery.start()
discovery.on('peer', () => {
deferStart.resolve()
})
await deferStart.promise
await discovery.stop()
discovery.on('peer', () => {
throw new Error('should not receive a peer event after stop')
})
await delay(2000)
})
})
}

10
src/peer-discovery/types.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export = PeerDiscovery;
import events from 'events';
declare class PeerDiscovery extends events.EventEmitter {
constructor (options: Object);
start (): Promise<void>;
stop (): Promise<void>;
tag: string;
}

View File

@ -30,12 +30,17 @@ TBD
A valid (read: that follows this abstraction) Peer Routing module must implement the following API.
### `.findPeers` - Find peers 'responsible' or 'closest' to a given key
### findPeer
- `Node.js` peerRouting.findPeers(key, function (err, peersPriorityQueue) {})
- `findPeer(peerId)`
In a peer to peer context, the concept of 'responsability' or 'closeness' for a given key translates to having a way to find deterministically or that at least there is a significant overlap between searches, the same group of peers when searching for the same given key.
Query the network for all multiaddresses associated with a `PeerId`.
This method will query the network (route it) and return a Priority Queue datastructe with a list of PeerInfo objects, ordered by 'closeness'.
**Parameters**
- [peerId](https://github.com/libp2p/js-peer-id).
key is a multihash
**Returns**
It returns the [peerId](https://github.com/libp2p/js-peer-id) together with the known peers [multiaddrs](https://github.com/multiformats/js-multiaddr), as follows:
`Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>`

10
src/peer-routing/types.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export = PeerRouting;
import PeerId from 'peer-id'
import { Multiaddr } from 'multiaddr'
declare class PeerRouting {
constructor (options?: Object);
findPeer (peerId: PeerId, options?: Object): Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>;
getClosestPeers(key: Uint8Array, options?: Object): AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>;
}

255
src/pubsub/README.md Normal file
View File

@ -0,0 +1,255 @@
interface-pubsub
==================
The `interface-pubsub` contains the base implementation for a libp2p pubsub router implementation. This interface should be used to implement a pubsub router compatible with libp2p. It includes a test suite that pubsub routers should run, in order to ensure compatibility with libp2p.
Table of Contents
=================
* [Implementations using this interface](#implementations-using-this-interface)
* [Interface usage](#interface-usage)
* [Extend interface](#extend-interface)
* [Example](#example)
* [API](#api)
* [Constructor](#constructor)
* [new Pubsub(options)](#new-pubsuboptions)
* [Parameters](#parameters)
* [Start](#start)
* [pubsub.start()](#pubsubstart)
* [Returns](#returns)
* [Stop](#stop)
* [pubsub.stop()](#pubsubstop)
* [Returns](#returns-1)
* [Publish](#publish)
* [pubsub.publish(topics, message)](#pubsubpublishtopics-message)
* [Parameters](#parameters-1)
* [Returns](#returns-2)
* [Subscribe](#subscribe)
* [pubsub.subscribe(topic)](#pubsubsubscribetopic)
* [Parameters](#parameters-2)
* [Unsubscribe](#unsubscribe)
* [pubsub.unsubscribe(topic)](#pubsubunsubscribetopic)
* [Parameters](#parameters-3)
* [Get Topics](#get-topics)
* [pubsub.getTopics()](#pubsubgettopics)
* [Returns](#returns-3)
* [Get Peers Subscribed to a topic](#get-peers-subscribed-to-a-topic)
* [pubsub.getSubscribers(topic)](#pubsubgetsubscriberstopic)
* [Parameters](#parameters-4)
* [Returns](#returns-4)
* [Validate](#validate)
* [pubsub.validate(message)](#pubsubvalidatemessage)
* [Parameters](#parameters-5)
* [Returns](#returns-5)
* [Test suite usage](#test-suite-usage)
## Implementations using this interface
You can check the following implementations as examples for building your own pubsub router.
- [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub)
- [ChainSafe/js-libp2p-gossipsub](https://github.com/ChainSafe/js-libp2p-gossipsub)
## Interface usage
`interface-pubsub` abstracts the implementation protocol registration within `libp2p` and takes care of all the protocol connections and streams, as well as the subscription management and the features describe in the libp2p [pubsub specs](https://github.com/libp2p/specs/tree/master/pubsub). This way, a pubsub implementation can focus on its message routing algorithm, instead of also needing to create the setup for it.
### Extend interface
A pubsub router implementation should start by extending the `interface-pubsub` class and **MUST** override the `_publish` function, according to the router algorithms. This function is responsible for forwarding publish messages to other peers, as well as forwarding received messages if the router provides the `canRelayMessage` option to the base implementation.
Other functions, such as `start`, `stop`, `subscribe`, `unsubscribe`, `_encodeRpc`, `_decodeRpc`, `_processRpcMessage`, `_addPeer` and `_removePeer` may be overwritten if the pubsub implementation needs to customize their logic. Implementations overriding these functions **MUST** call `super`.
The `start` and `stop` functions are responsible for the registration of the pubsub protocol with libp2p. The `stop` function also guarantees that the open streams in the protocol are properly closed.
The `subscribe` and `unsubscribe` functions take care of the subscription management and its inherent message propagation.
When using a custom protobuf definition for message marshalling, you should override `_encodeRpc` and `_decodeRpc` to use the new protobuf instead of the default one.
`_processRpcMessage` is responsible for handling messages received from other peers. This should be extended if further operations/validations are needed by the router.
The `_addPeer` and `_removePeer` functions are called when new peers running the pubsub router protocol establish a connection with the peer. They are used for tracking the open streams between the peers.
All the remaining functions **MUST NOT** be overwritten.
### Example
The following example aims to show how to create your pubsub implementation extending this base protocol. The pubsub implementation will handle the subscriptions logic.
```JavaScript
const Pubsub = require('libp2p-interfaces/src/pubsub')
class PubsubImplementation extends Pubsub {
constructor({ libp2p, options })
super({
debugName: 'libp2p:pubsub',
multicodecs: '/pubsub-implementation/1.0.0',
libp2p,
globalSigningPolicy: options.globalSigningPolicy
})
}
_publish (message) {
// Required to be implemented by the subclass
// Routing logic for the message
}
}
```
## API
The interface aims to specify a common interface that all pubsub router implementation should follow. A pubsub router implementation should extend the [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). When peers receive pubsub messages, these messages will be emitted by the event emitter where the `eventName` will be the `topic` associated with the message.
### Constructor
The base class constructor configures the pubsub instance for use with a libp2p instance. It includes settings for logging, signature policies, etc.
#### `new Pubsub({options})`
##### Parameters
| Name | Type | Description | Default |
|------|------|-------------|---------|
| options.libp2p | `Libp2p` | libp2p instance | required, no default |
| options.debugName | `string` | log namespace | required, no default |
| options.multicodecs | `string \| Array<string>` | protocol identifier(s) | required, no default |
| options.globalSignaturePolicy | `'StrictSign' \| 'StrictNoSign'` | signature policy to be globally applied | `'StrictSign'` |
| options.canRelayMessage | `boolean` | if can relay messages if not subscribed | `false` |
| options.emitSelf | `boolean` | if `publish` should emit to self, if subscribed | `false` |
### 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()`
### 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()`
### Publish
Publish data message to pubsub topics.
#### `pubsub.publish(topic, message)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
| message | `Uint8Array` | message to publish |
##### Returns
| Type | Description |
|------|-------------|
| `Promise<void>` | resolves once the message is published to the network |
### Subscribe
Subscribe to the given topic.
#### `pubsub.subscribe(topic)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
### Unsubscribe
Unsubscribe from the given topic.
#### `pubsub.unsubscribe(topic)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
### Get Topics
Get the list of topics which the peer is subscribed to.
#### `pubsub.getTopics()`
##### Returns
| Type | Description |
|------|-------------|
| `Array<String>` | 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<string>` | Array of base-58 PeerId's |
### Validate
Validates a message according to the signature policy and topic-specific validation function.
#### `pubsub.validate(message)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| message | `Message` | a pubsub message |
#### Returns
| Type | Description |
|------|-------------|
| `Promise<void>` | resolves if the message is valid |
## Test suite usage
```js
'use strict'
const tests = require('libp2p-interfaces/src/pubsub/tests')
const YourPubsubRouter = require('../src')
describe('compliance', () => {
let peers
let pubsubNodes = []
tests({
async setup (number = 1, options = {}) {
// Create number pubsub nodes with libp2p
peers = await createPeers({ number })
peers.forEach((peer) => {
const ps = new YourPubsubRouter(peer, options)
pubsubNodes.push(ps)
})
return pubsubNodes
},
async teardown () {
// Clean up any resources created by setup()
await Promise.all(pubsubNodes.map(ps => ps.stop()))
peers.length && await Promise.all(peers.map(peer => peer.stop()))
}
})
})
```

46
src/pubsub/errors.js Normal file
View File

@ -0,0 +1,46 @@
'use strict'
exports.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 `signature` is invalid
*/
ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE',
// 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'
}

751
src/pubsub/index.js Normal file
View File

@ -0,0 +1,751 @@
'use strict'
const debug = require('debug')
/** @typedef {import('../types').EventEmitterFactory} Events */
/** @type Events */
const EventEmitter = require('events')
const errcode = require('err-code')
const { pipe } = require('it-pipe')
const MulticodecTopology = require('../topology/multicodec-topology')
const { codes } = require('./errors')
const message = require('./message')
const PeerStreams = require('./peer-streams')
const { SignaturePolicy } = require('./signature-policy')
const utils = require('./utils')
const {
signMessage,
verifySignature
} = require('./message/sign')
/**
* @typedef {any} Libp2p
* @typedef {import('peer-id')} PeerId
* @typedef {import('bl')} BufferList
* @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream
* @typedef {import('../connection/connection')} Connection
* @typedef {import('./message/types').RPC} RPC
* @typedef {import('./message/types').SubOpts} RPCSubOpts
* @typedef {import('./message/types').Message} RPCMessage
* @typedef {import('./signature-policy').SignaturePolicyType} SignaturePolicyType
*/
/**
* @typedef {Object} InMessage
* @property {string} [from]
* @property {string} receivedFrom
* @property {string[]} topicIDs
* @property {Uint8Array} [seqno]
* @property {Uint8Array} data
* @property {Uint8Array} [signature]
* @property {Uint8Array} [key]
*
* @typedef {Object} PubsubProperties
* @property {string} debugName - log namespace
* @property {Array<string>|string} multicodecs - protocol identificers to connect
* @property {Libp2p} libp2p
*
* @typedef {Object} PubsubOptions
* @property {SignaturePolicyType} [globalSignaturePolicy = SignaturePolicy.StrictSign] - defines how signatures should be handled
* @property {boolean} [canRelayMessage = false] - if can relay messages not subscribed
* @property {boolean} [emitSelf = false] - if publish should emit to self, if subscribed
*/
/**
* PubsubBaseProtocol handles the peers and connections logic for pubsub routers
* and specifies the API that pubsub routers should have.
*/
class PubsubBaseProtocol extends EventEmitter {
/**
* @param {PubsubProperties & PubsubOptions} props
* @abstract
*/
constructor ({
debugName,
multicodecs,
libp2p,
globalSignaturePolicy = SignaturePolicy.StrictSign,
canRelayMessage = false,
emitSelf = false
}) {
if (typeof debugName !== 'string') {
throw new Error('a debugname `string` is required')
}
if (!multicodecs) {
throw new Error('multicodecs are required')
}
if (!libp2p) {
throw new Error('libp2p is required')
}
super()
this.log = Object.assign(debug(debugName), {
err: debug(`${debugName}:error`)
})
/**
* @type {Array<string>}
*/
this.multicodecs = utils.ensureArray(multicodecs)
this._libp2p = libp2p
this.registrar = libp2p.registrar
/**
* @type {PeerId}
*/
this.peerId = libp2p.peerId
this.started = false
/**
* Map of topics to which peers are subscribed to
*
* @type {Map<string, Set<string>>}
*/
this.topics = new Map()
/**
* List of our subscriptions
*
* @type {Set<string>}
*/
this.subscriptions = new Set()
/**
* Map of peer streams
*
* @type {Map<string, import('./peer-streams')>}
*/
this.peers = new Map()
// validate signature policy
if (!SignaturePolicy[globalSignaturePolicy]) {
throw errcode(new Error('Invalid global signature policy'), codes.ERR_INVALID_SIGNATURE_POLICY)
}
/**
* The signature policy to follow by default
*
* @type {string}
*/
this.globalSignaturePolicy = globalSignaturePolicy
/**
* If router can relay received messages, even if not subscribed
*
* @type {boolean}
*/
this.canRelayMessage = canRelayMessage
/**
* if publish should emit to self, if subscribed
*
* @type {boolean}
*/
this.emitSelf = emitSelf
/**
* Topic validator function
*
* @typedef {function(string, InMessage): Promise<void>} validator
*/
/**
* Topic validator map
*
* Keyed by topic
* Topic validators are functions with the following input:
*
* @type {Map<string, validator>}
*/
this.topicValidators = new Map()
this._registrarId = undefined
this._onIncomingStream = this._onIncomingStream.bind(this)
this._onPeerConnected = this._onPeerConnected.bind(this)
this._onPeerDisconnected = this._onPeerDisconnected.bind(this)
}
// LIFECYCLE METHODS
/**
* Register the pubsub protocol onto the libp2p node.
*
* @returns {void}
*/
start () {
if (this.started) {
return
}
this.log('starting')
// Incoming streams
// Called after a peer dials us
this.registrar.handle(this.multicodecs, this._onIncomingStream)
// register protocol with topology
// Topology callbacks called on connection manager changes
const topology = new MulticodecTopology({
multicodecs: this.multicodecs,
handlers: {
onConnect: this._onPeerConnected,
onDisconnect: this._onPeerDisconnected
}
})
this._registrarId = this.registrar.register(topology)
this.log('started')
this.started = true
}
/**
* Unregister the pubsub protocol and the streams with other peers will be closed.
*
* @returns {void}
*/
stop () {
if (!this.started) {
return
}
// unregister protocol and handlers
this.registrar.unregister(this._registrarId)
this.log('stopping')
this.peers.forEach((peerStreams) => peerStreams.close())
this.peers = new Map()
this.subscriptions = new Set()
this.started = false
this.log('stopped')
}
/**
* On an inbound stream opened.
*
* @protected
* @param {Object} props
* @param {string} props.protocol
* @param {MuxedStream} props.stream
* @param {Connection} props.connection - connection
*/
_onIncomingStream ({ protocol, stream, connection }) {
const peerId = connection.remotePeer
const idB58Str = peerId.toB58String()
const peer = this._addPeer(peerId, protocol)
const inboundStream = peer.attachInboundStream(stream)
this._processMessages(idB58Str, inboundStream, peer)
}
/**
* Registrar notifies an established connection with pubsub protocol.
*
* @protected
* @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)
try {
const { stream, protocol } = await conn.newStream(this.multicodecs)
const peer = this._addPeer(peerId, protocol)
await peer.attachOutboundStream(stream)
} catch (err) {
this.log.err(err)
}
// Immediately send my own subscriptions to the newly established conn
this._sendSubscriptions(idB58Str, Array.from(this.subscriptions), true)
}
/**
* Registrar notifies a closing connection with pubsub protocol.
*
* @protected
* @param {PeerId} peerId - peerId
* @param {Error} [err] - error for connection end
*/
_onPeerDisconnected (peerId, err) {
const idB58Str = peerId.toB58String()
this.log('connection ended', idB58Str, err ? err.message : '')
this._removePeer(peerId)
}
/**
* Notifies the router that a peer has been connected
*
* @protected
* @param {PeerId} peerId
* @param {string} protocol
* @returns {PeerStreams}
*/
_addPeer (peerId, protocol) {
const id = peerId.toB58String()
const existing = this.peers.get(id)
// If peer streams already exists, do nothing
if (existing) {
return existing
}
// else create a new peer streams
this.log('new peer', id)
const peerStreams = new PeerStreams({
id: peerId,
protocol
})
this.peers.set(id, peerStreams)
peerStreams.once('close', () => this._removePeer(peerId))
return peerStreams
}
/**
* Notifies the router that a peer has been disconnected.
*
* @protected
* @param {PeerId} peerId
* @returns {PeerStreams | undefined}
*/
_removePeer (peerId) {
if (!peerId) return
const id = peerId.toB58String()
const peerStreams = this.peers.get(id)
if (!peerStreams) return
// close peer streams
peerStreams.removeAllListeners()
peerStreams.close()
// delete peer streams
this.log('delete peer', id)
this.peers.delete(id)
// remove peer from topics map
for (const peers of this.topics.values()) {
peers.delete(id)
}
return peerStreams
}
// MESSAGE METHODS
/**
* Responsible for processing each RPC message received by other peers.
*
* @param {string} idB58Str - peer id string in base58
* @param {AsyncIterable<Uint8Array|BufferList>} stream - inbound stream
* @param {PeerStreams} peerStreams - PubSub peer
* @returns {Promise<void>}
*/
async _processMessages (idB58Str, stream, peerStreams) {
try {
await pipe(
stream,
async (source) => {
for await (const data of source) {
const rpcBytes = data instanceof Uint8Array ? data : data.slice()
const rpcMsg = this._decodeRpc(rpcBytes)
this._processRpc(idB58Str, peerStreams, rpcMsg)
}
}
)
} catch (err) {
this._onPeerDisconnected(peerStreams.id, err)
}
}
/**
* Handles an rpc request from a peer
*
* @param {string} idB58Str
* @param {PeerStreams} peerStreams
* @param {RPC} rpc
* @returns {boolean}
*/
_processRpc (idB58Str, peerStreams, rpc) {
this.log('rpc from', idB58Str)
const subs = rpc.subscriptions
const msgs = rpc.msgs
if (subs.length) {
// update peer subscriptions
subs.forEach((/** @type {RPCSubOpts} */ subOpt) => {
this._processRpcSubOpt(idB58Str, subOpt)
})
this.emit('pubsub:subscription-change', peerStreams.id, subs)
}
if (!this._acceptFrom(idB58Str)) {
this.log('received message from unacceptable peer %s', idB58Str)
return false
}
if (msgs.length) {
// @ts-ignore RPC message is modified
msgs.forEach((message) => {
if (!(this.canRelayMessage || message.topicIDs.some((/** @type {string} */ topic) => this.subscriptions.has(topic)))) {
this.log('received message we didn\'t subscribe to. Dropping.')
return
}
const msg = utils.normalizeInRpcMessage(message, idB58Str)
this._processRpcMessage(msg)
})
}
return true
}
/**
* Handles a subscription change from a peer
*
* @param {string} id
* @param {RPCSubOpts} subOpt
*/
_processRpcSubOpt (id, subOpt) {
const t = subOpt.topicID
let topicSet = this.topics.get(t)
if (!topicSet) {
topicSet = new Set()
this.topics.set(t, topicSet)
}
if (subOpt.subscribe) {
// subscribe peer to new topic
topicSet.add(id)
} else {
// unsubscribe from existing topic
topicSet.delete(id)
}
}
/**
* Handles an message from a peer
*
* @param {InMessage} msg
* @returns {Promise<void>}
*/
async _processRpcMessage (msg) {
if (this.peerId.toB58String() === msg.from && !this.emitSelf) {
return
}
// Ensure the message is valid before processing it
try {
await this.validate(msg)
} catch (err) {
this.log('Message is invalid, dropping it. %O', err)
return
}
// Emit to self
this._emitMessage(msg)
this._publish(utils.normalizeOutRpcMessage(msg))
}
/**
* Emit a message from a peer
*
* @param {InMessage} message
*/
_emitMessage (message) {
message.topicIDs.forEach((topic) => {
if (this.subscriptions.has(topic)) {
this.emit(topic, message)
}
})
}
/**
* The default msgID implementation
* Child class can override this.
*
* @param {RPCMessage} msg - the message object
* @returns {Uint8Array} message id as bytes
*/
getMsgId (msg) {
const signaturePolicy = this.globalSignaturePolicy
switch (signaturePolicy) {
case SignaturePolicy.StrictSign:
return utils.msgId(msg.from, msg.seqno)
case SignaturePolicy.StrictNoSign:
return utils.noSignMsgId(msg.data)
default:
throw errcode(new Error('Cannot get message id: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
}
}
/**
* Whether to accept a message from a peer
* Override to create a graylist
*
* @override
* @param {string} id
* @returns {boolean}
*/
_acceptFrom (id) {
return true
}
/**
* Decode Uint8Array into an RPC object.
* This can be override to use a custom router protobuf.
*
* @param {Uint8Array} bytes
* @returns {RPC}
*/
_decodeRpc (bytes) {
return message.rpc.RPC.decode(bytes)
}
/**
* Encode RPC object into a Uint8Array.
* This can be override to use a custom router protobuf.
*
* @param {RPC} rpc
* @returns {Uint8Array}
*/
_encodeRpc (rpc) {
return message.rpc.RPC.encode(rpc)
}
/**
* Send an rpc object to a peer
*
* @param {string} id - peer id
* @param {RPC} rpc
* @returns {void}
*/
_sendRpc (id, rpc) {
const peerStreams = this.peers.get(id)
if (!peerStreams || !peerStreams.isWritable) {
const msg = `Cannot send RPC to ${id} as there is no open stream to it available`
this.log.err(msg)
return
}
peerStreams.write(this._encodeRpc(rpc))
}
/**
* Send subscroptions to a peer
*
* @param {string} id - peer id
* @param {string[]} topics
* @param {boolean} subscribe - set to false for unsubscriptions
* @returns {void}
*/
_sendSubscriptions (id, topics, subscribe) {
return this._sendRpc(id, {
subscriptions: topics.map(t => ({ topicID: t, subscribe: subscribe }))
})
}
/**
* Validates the given message. The signature will be checked for authenticity.
* Throws an error on invalid messages
*
* @param {InMessage} message
* @returns {Promise<void>}
*/
async validate (message) { // eslint-disable-line require-await
const signaturePolicy = this.globalSignaturePolicy
switch (signaturePolicy) {
case SignaturePolicy.StrictNoSign:
if (message.from) {
throw errcode(new Error('StrictNoSigning: from should not be present'), codes.ERR_UNEXPECTED_FROM)
}
if (message.signature) {
throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE)
}
if (message.key) {
throw errcode(new Error('StrictNoSigning: key should not be present'), codes.ERR_UNEXPECTED_KEY)
}
if (message.seqno) {
throw errcode(new Error('StrictNoSigning: seqno should not be present'), codes.ERR_UNEXPECTED_SEQNO)
}
break
case SignaturePolicy.StrictSign:
if (!message.signature) {
throw errcode(new Error('StrictSigning: Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE)
}
if (!message.seqno) {
throw errcode(new Error('StrictSigning: Signing required and no seqno was present'), codes.ERR_MISSING_SEQNO)
}
if (!(await verifySignature(message))) {
throw errcode(new Error('StrictSigning: Invalid message signature'), codes.ERR_INVALID_SIGNATURE)
}
break
default:
throw errcode(new Error('Cannot validate message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
}
for (const topic of message.topicIDs) {
const validatorFn = this.topicValidators.get(topic)
if (!validatorFn) {
continue // eslint-disable-line
}
await validatorFn(topic, message)
}
}
/**
* Normalizes the message and signs it, if signing is enabled.
* Should be used by the routers to create the message to send.
*
* @protected
* @param {RPCMessage} message
* @returns {Promise<RPCMessage>}
*/
_buildMessage (message) {
const signaturePolicy = this.globalSignaturePolicy
switch (signaturePolicy) {
case SignaturePolicy.StrictSign:
message.from = this.peerId.toB58String()
message.seqno = utils.randomSeqno()
return signMessage(this.peerId, utils.normalizeOutRpcMessage(message))
case SignaturePolicy.StrictNoSign:
return message
default:
throw errcode(new Error('Cannot build message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
}
}
// API METHODS
/**
* Get a list of the peer-ids that are subscribed to one topic.
*
* @param {string} topic
* @returns {Array<string>}
*/
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')
}
const peersInTopic = this.topics.get(topic)
if (!peersInTopic) {
return []
}
return Array.from(peersInTopic)
}
/**
* Publishes messages to all subscribed peers
*
* @override
* @param {string} topic
* @param {Uint8Array} message
* @returns {Promise<void>}
*/
async publish (topic, message) {
if (!this.started) {
throw new Error('Pubsub has not started')
}
this.log('publish', topic, message)
const from = this.peerId.toB58String()
let msgObject = {
receivedFrom: from,
data: message,
topicIDs: [topic]
}
// ensure that the message follows the signature policy
const outMsg = await this._buildMessage(msgObject)
msgObject = utils.normalizeInRpcMessage(outMsg)
// Emit to self if I'm interested and emitSelf enabled
this.emitSelf && this._emitMessage(msgObject)
// send to all the other peers
await this._publish(msgObject)
}
/**
* 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 {InMessage} message
* @returns {Promise<void>}
*
*/
_publish (message) {
throw errcode(new Error('publish must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED')
}
/**
* Subscribes to a given topic.
*
* @abstract
* @param {string} topic
* @returns {void}
*/
subscribe (topic) {
if (!this.started) {
throw new Error('Pubsub has not started')
}
if (!this.subscriptions.has(topic)) {
this.subscriptions.add(topic)
this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], true))
}
}
/**
* Unsubscribe from the given topic.
*
* @override
* @param {string} topic
* @returns {void}
*/
unsubscribe (topic) {
if (!this.started) {
throw new Error('Pubsub is not started')
}
if (this.subscriptions.has(topic) && this.listenerCount(topic) === 0) {
this.subscriptions.delete(topic)
this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], false))
}
}
/**
* Get the list of topics which the peer is subscribed to.
*
* @override
* @returns {Array<string>}
*/
getTopics () {
if (!this.started) {
throw new Error('Pubsub is not started')
}
return Array.from(this.subscriptions)
}
}
PubsubBaseProtocol.message = message
PubsubBaseProtocol.utils = utils
PubsubBaseProtocol.SignaturePolicy = SignaturePolicy
module.exports = PubsubBaseProtocol

View File

@ -0,0 +1,16 @@
'use strict'
// @ts-ignore protons not typed
const protons = require('protons')
const rpcProto = protons(require('./rpc.proto.js'))
const RPC = rpcProto.RPC
const topicDescriptorProto = protons(require('./topic-descriptor.proto.js'))
module.exports = {
rpc: rpcProto,
td: topicDescriptorProto,
RPC,
Message: RPC.Message,
SubOpts: RPC.SubOpts
}

View File

@ -0,0 +1,20 @@
'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;
}
}`

100
src/pubsub/message/sign.js Normal file
View File

@ -0,0 +1,100 @@
'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<Message>}
*/
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 {InMessage} message
* @returns {Promise<boolean>}
*/
async function verifySignature (message) {
if (!message.signature) {
throw new Error('Message must contain a signature to be verified')
}
// Get message sans the signature
const bytes = uint8ArrayConcat([
SignPrefix,
Message.encode({
...message,
from: message.from && PeerId.createFromCID(message.from).toBytes(),
signature: undefined,
key: undefined
})
])
// 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 {InMessage} message
* @returns {Promise<PublicKey>}
*/
async function messagePublicKey (message) {
// should be available in the from property of the message (peer id)
if (!message.from) {
throw new Error('Could not get the public key from the originator id')
}
const from = PeerId.createFromCID(message.from)
if (message.key) {
const keyPeerId = await PeerId.createFromPubKey(message.key)
// the key belongs to the sender, return the key
if (keyPeerId.equals(from)) return keyPeerId.pubKey
// We couldn't validate pubkey is from the originator, error
throw new Error('Public Key does not match the originator')
} else if (from.pubKey) {
return from.pubKey
} else {
throw new Error('Could not get the public key from the originator id')
}
}
/**
* @typedef {import('..').InMessage} InMessage
* @typedef {import('libp2p-crypto').PublicKey} PublicKey
*/
module.exports = {
messagePublicKey,
signMessage,
SignPrefix,
verifySignature
}

View File

@ -0,0 +1,30 @@
'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
}
}
}`

5
src/pubsub/message/types.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { RPC, Message, SubOpts } from './types'
export type RPC = RPC
export type Message = Message
export type SubOpts = SubOpts

205
src/pubsub/peer-streams.js Normal file
View File

@ -0,0 +1,205 @@
'use strict'
const debug = require('debug')
const log = Object.assign(debug('libp2p-pubsub:peer-streams'), {
error: debug('libp2p-pubsub:peer-streams:err')
})
/** @typedef {import('../types').EventEmitterFactory} Events */
/** @type Events */
const EventEmitter = require('events')
const lp = require('it-length-prefixed')
const pushable = require('it-pushable')
const { pipe } = require('it-pipe')
const { source: abortable } = require('abortable-iterator')
const AbortController = require('abort-controller').default
/**
* @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream
* @typedef {import('peer-id')} PeerId
* @typedef {import('it-pushable').Pushable<Uint8Array>} PushableStream
*/
/**
* Thin wrapper around a peer's inbound / outbound pubsub streams
*/
class PeerStreams extends EventEmitter {
/**
* @param {object} properties - properties of the PeerStreams.
* @param {PeerId} properties.id
* @param {string} properties.protocol
*/
constructor ({ id, protocol }) {
super()
/**
* @type {import('peer-id')}
*/
this.id = id
/**
* Established protocol
*
* @type {string}
*/
this.protocol = protocol
/**
* The raw outbound stream, as retrieved from conn.newStream
*
* @private
* @type {null|MuxedStream}
*/
this._rawOutboundStream = null
/**
* The raw inbound stream, as retrieved from the callback from libp2p.handle
*
* @private
* @type {null|MuxedStream}
*/
this._rawInboundStream = null
/**
* An AbortController for controlled shutdown of the inbound stream
*
* @private
* @type {AbortController}
*/
this._inboundAbortController = new AbortController()
/**
* Write stream -- its preferable to use the write method
*
* @type {null|PushableStream}
*/
this.outboundStream = null
/**
* Read stream
*
* @type {null| AsyncIterable<Uint8Array>}
*/
this.inboundStream = null
}
/**
* Do we have a connection to read from?
*
* @type {boolean}
*/
get isReadable () {
return Boolean(this.inboundStream)
}
/**
* Do we have a connection to write on?
*
* @type {boolean}
*/
get isWritable () {
return Boolean(this.outboundStream)
}
/**
* Send a message to this peer.
* Throws if there is no `stream` to write to available.
*
* @param {Uint8Array} data
* @returns {void}
*/
write (data) {
if (!this.outboundStream) {
const id = this.id.toB58String()
throw new Error('No writable connection to ' + id)
}
this.outboundStream.push(data)
}
/**
* Attach a raw inbound stream and setup a read stream
*
* @param {MuxedStream} stream
* @returns {AsyncIterable<Uint8Array>}
*/
attachInboundStream (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 = abortable(
pipe(
this._rawInboundStream,
lp.decode()
),
this._inboundAbortController.signal,
{ returnOnAbort: true }
)
this.emit('stream:inbound')
return this.inboundStream
}
/**
* Attach a raw outbound stream and setup a write stream
*
* @param {MuxedStream} stream
* @returns {Promise<void>}
*/
async attachOutboundStream (stream) {
// If an outbound stream already exists, gently close it
const _prevStream = this.outboundStream
if (this.outboundStream) {
// 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
this._rawOutboundStream && this._rawOutboundStream.reset && this._rawOutboundStream.reset()
this._rawOutboundStream = null
this.outboundStream = null
if (shouldEmit) {
this.emit('close')
}
}
})
pipe(
this.outboundStream,
lp.encode(),
this._rawOutboundStream
).catch(/** @param {Error} err */ err => {
log.error(err)
})
// Only emit if the connection is new
if (!_prevStream) {
this.emit('stream:outbound')
}
}
/**
* Closes the open connection to peer
*
* @returns {void}
*/
close () {
// End the outbound stream
if (this.outboundStream) {
this.outboundStream.end()
}
// End the inbound stream
if (this.inboundStream) {
this._inboundAbortController.abort()
}
this._rawOutboundStream = null
this.outboundStream = null
this._rawInboundStream = null
this.inboundStream = null
this.emit('close')
}
}
module.exports = PeerStreams

View File

@ -0,0 +1,33 @@
'use strict'
/**
* Enum for Signature Policy
* Details how message signatures are produced/consumed
*/
const SignaturePolicy = {
/**
* On the producing side:
* * Build messages with the signature, key (from may be enough for certain inlineable public key types), from and seqno fields.
*
* On the consuming side:
* * Enforce the fields to be present, reject otherwise.
* * Propagate only if the fields are valid and signature can be verified, reject otherwise.
*/
StrictSign: /** @type {'StrictSign'} */ ('StrictSign'),
/**
* On the producing side:
* * Build messages without the signature, key, from and seqno fields.
* * The corresponding protobuf key-value pairs are absent from the marshalled message, not just empty.
*
* On the consuming side:
* * Enforce the fields to be absent, reject otherwise.
* * Propagate only if the fields are absent, reject otherwise.
* * A message_id function will not be able to use the above fields, and should instead rely on the data field. A commonplace strategy is to calculate a hash.
*/
StrictNoSign: /** @type {'StrictNoSign'} */ ('StrictNoSign')
}
exports.SignaturePolicy = SignaturePolicy
/**
* @typedef {SignaturePolicy[keyof SignaturePolicy]} SignaturePolicyType
*/

94
src/pubsub/tests/api.js Normal file
View File

@ -0,0 +1,94 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const topic = 'foo'
const data = uint8ArrayFromString('bar')
module.exports = (common) => {
describe('pubsub api', () => {
let pubsub
// Create pubsub router
beforeEach(async () => {
[pubsub] = await common.setup(1)
})
afterEach(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('can start correctly', () => {
sinon.spy(pubsub.registrar, '_handle')
sinon.spy(pubsub.registrar, 'register')
pubsub.start()
expect(pubsub.started).to.eql(true)
expect(pubsub.registrar._handle.callCount).to.eql(1)
expect(pubsub.registrar.register.callCount).to.eql(1)
})
it('can stop correctly', () => {
sinon.spy(pubsub.registrar, 'unregister')
pubsub.start()
pubsub.stop()
expect(pubsub.started).to.eql(false)
expect(pubsub.registrar.unregister.callCount).to.eql(1)
})
it('can subscribe and unsubscribe correctly', async () => {
const handler = () => {
throw new Error('a message should not be received')
}
pubsub.start()
pubsub.subscribe(topic)
pubsub.on('topic', handler)
await pWaitFor(() => {
const topics = pubsub.getTopics()
return topics.length === 1 && topics[0] === topic
})
pubsub.unsubscribe(topic)
await pWaitFor(() => !pubsub.getTopics().length)
// Publish to guarantee the handler is not called
await pubsub.publish(topic, data)
pubsub.stop()
})
it('can subscribe and publish correctly', async () => {
const defer = pDefer()
const handler = (msg) => {
expect(msg).to.not.eql(undefined)
defer.resolve()
}
pubsub.start()
pubsub.subscribe(topic)
pubsub.on(topic, handler)
await pubsub.publish(topic, data)
await defer.promise
pubsub.stop()
})
})
}

View File

@ -0,0 +1,70 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const uint8ArrayFromString = require('uint8arrays/from-string')
const topic = 'foo'
const data = uint8ArrayFromString('bar')
const shouldNotHappen = (_) => expect.fail()
module.exports = (common) => {
describe('emit self', () => {
let pubsub
describe('enabled', () => {
before(async () => {
[pubsub] = await common.setup(1, { emitSelf: true })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('should emit to self on publish', () => {
const promise = new Promise((resolve) => pubsub.once(topic, resolve))
pubsub.publish(topic, data)
return promise
})
})
describe('disabled', () => {
before(async () => {
[pubsub] = await common.setup(1, { emitSelf: false })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('should not emit to self on publish', () => {
pubsub.once(topic, (m) => shouldNotHappen)
pubsub.publish(topic, data)
// Wait 1 second to guarantee that self is not noticed
return new Promise((resolve) => setTimeout(resolve, 1000))
})
})
})
}

19
src/pubsub/tests/index.js Normal file
View File

@ -0,0 +1,19 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const apiTest = require('./api')
const emitSelfTest = require('./emit-self')
const messagesTest = require('./messages')
const twoNodesTest = require('./two-nodes')
const multipleNodesTest = require('./multiple-nodes')
module.exports = (common) => {
describe('interface-pubsub', () => {
apiTest(common)
emitSelfTest(common)
messagesTest(common)
twoNodesTest(common)
multipleNodesTest(common)
})
}

View File

@ -0,0 +1,116 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const PeerId = require('peer-id')
const uint8ArrayFromString = require('uint8arrays/from-string')
const { utils } = require('..')
const PeerStreams = require('../peer-streams')
const { SignaturePolicy } = require('../signature-policy')
const topic = 'foo'
const data = uint8ArrayFromString('bar')
module.exports = (common) => {
describe('messages', () => {
let pubsub
// Create pubsub router
beforeEach(async () => {
[pubsub] = await common.setup(1)
pubsub.start()
})
afterEach(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('should emit normalized signed messages on publish', async () => {
pubsub.globalSignaturePolicy = SignaturePolicy.StrictSign
sinon.spy(pubsub, '_emitMessage')
await pubsub.publish(topic, data)
expect(pubsub._emitMessage.callCount).to.eql(1)
const [messageToEmit] = pubsub._emitMessage.getCall(0).args
expect(messageToEmit.seqno).to.not.eql(undefined)
expect(messageToEmit.key).to.not.eql(undefined)
expect(messageToEmit.signature).to.not.eql(undefined)
})
it('should drop unsigned messages', async () => {
sinon.spy(pubsub, '_emitMessage')
sinon.spy(pubsub, '_publish')
sinon.spy(pubsub, 'validate')
const peerStream = new PeerStreams({
id: await PeerId.create(),
protocol: 'test'
})
const rpc = {
subscriptions: [],
msgs: [{
receivedFrom: peerStream.id.toB58String(),
from: peerStream.id.toBytes(),
data,
seqno: utils.randomSeqno(),
topicIDs: [topic]
}]
}
pubsub.subscribe(topic)
pubsub._processRpc(peerStream.id.toB58String(), peerStream, rpc)
return new Promise((resolve) => {
setTimeout(() => {
expect(pubsub.validate.callCount).to.eql(1)
expect(pubsub._emitMessage.called).to.eql(false)
expect(pubsub._publish.called).to.eql(false)
resolve()
}, 50)
})
})
it('should not drop unsigned messages if strict signing is disabled', async () => {
pubsub.globalSignaturePolicy = SignaturePolicy.StrictNoSign
sinon.spy(pubsub, '_emitMessage')
sinon.spy(pubsub, '_publish')
sinon.spy(pubsub, 'validate')
const peerStream = new PeerStreams({
id: await PeerId.create(),
protocol: 'test'
})
const rpc = {
subscriptions: [],
msgs: [{
data,
topicIDs: [topic]
}]
}
pubsub.subscribe(topic)
pubsub._processRpc(peerStream.id.toB58String(), peerStream, rpc)
return new Promise((resolve) => {
setTimeout(() => {
expect(pubsub.validate.callCount).to.eql(1)
expect(pubsub._emitMessage.called).to.eql(true)
expect(pubsub._publish.called).to.eql(true)
resolve()
}, 50)
})
})
})
}

View File

@ -0,0 +1,341 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const delay = require('delay')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')
const { expectSet } = require('./utils')
module.exports = (common) => {
describe('pubsub with multiple nodes', function () {
this.timeout(10e3)
describe('every peer subscribes to the topic', () => {
describe('line', () => {
// line
// ◉────◉────◉
// a b c
let psA, psB, psC
// Create and start pubsub nodes
beforeEach(async () => {
[psA, psB, psC] = await common.setup(3)
// Start pubsub mpdes
;[psA, psB, psC].map((p) => p.start())
})
// Connect nodes
beforeEach(async () => {
await psA._libp2p.dial(psB.peerId)
await psB._libp2p.dial(psC.peerId)
// Wait for peers to be ready in pubsub
await pWaitFor(() =>
psA.peers.size === 1 &&
psC.peers.size === 1 &&
psA.peers.size === 1
)
})
afterEach(async () => {
sinon.restore()
;[psA, psB, psC].map((p) => p.stop())
await common.teardown()
})
it('subscribe to the topic on node a', async () => {
const topic = 'Z'
psA.subscribe(topic)
expectSet(psA.subscriptions, [topic])
await new Promise((resolve) => psB.once('pubsub:subscription-change', resolve))
expect(psB.peers.size).to.equal(2)
const aPeerId = psA.peerId.toB58String()
expectSet(psB.topics.get(topic), [aPeerId])
expect(psC.peers.size).to.equal(1)
expect(psC.topics.get(topic)).to.eql(undefined)
})
it('subscribe to the topic on node b', async () => {
const topic = 'Z'
psB.subscribe(topic)
expectSet(psB.subscriptions, [topic])
await Promise.all([
new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psC.once('pubsub:subscription-change', resolve))
])
expect(psA.peers.size).to.equal(1)
expectSet(psA.topics.get(topic), [psB.peerId.toB58String()])
expect(psC.peers.size).to.equal(1)
expectSet(psC.topics.get(topic), [psB.peerId.toB58String()])
})
it('subscribe to the topic on node c', () => {
const topic = 'Z'
const defer = pDefer()
psC.subscribe(topic)
expectSet(psC.subscriptions, [topic])
psB.once('pubsub:subscription-change', () => {
expect(psA.peers.size).to.equal(1)
expect(psB.peers.size).to.equal(2)
expectSet(psB.topics.get(topic), [psC.peerId.toB58String()])
defer.resolve()
})
return defer.promise
})
it('publish on node a', async () => {
const topic = 'Z'
const defer = pDefer()
psA.subscribe(topic)
psB.subscribe(topic)
psC.subscribe(topic)
// await subscription change
await Promise.all([
new Promise(resolve => psA.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psB.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psC.once('pubsub:subscription-change', () => resolve(null)))
])
// await a cycle
await delay(1000)
let counter = 0
psA.on(topic, incMsg)
psB.on(topic, incMsg)
psC.on(topic, incMsg)
psA.publish(topic, uint8ArrayFromString('hey'))
function incMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('hey')
check()
}
function check () {
if (++counter === 3) {
psA.removeListener(topic, incMsg)
psB.removeListener(topic, incMsg)
psC.removeListener(topic, incMsg)
defer.resolve()
}
}
return defer.promise
})
// since the topology is the same, just the publish
// gets sent by other peer, we reused the same peers
describe('1 level tree', () => {
// 1 level tree
// ┌◉┐
// │b│
// ◉─┘ └─◉
// a c
it('publish on node b', async () => {
const topic = 'Z'
const defer = pDefer()
let counter = 0
psA.subscribe(topic)
psB.subscribe(topic)
psC.subscribe(topic)
// await subscription change
await Promise.all([
new Promise(resolve => psA.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psB.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psC.once('pubsub:subscription-change', () => resolve(null)))
])
psA.on(topic, incMsg)
psB.on(topic, incMsg)
psC.on(topic, incMsg)
// await a cycle
await delay(1000)
psB.publish(topic, uint8ArrayFromString('hey'))
function incMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('hey')
check()
}
function check () {
if (++counter === 3) {
psA.removeListener(topic, incMsg)
psB.removeListener(topic, incMsg)
psC.removeListener(topic, incMsg)
defer.resolve()
}
}
return defer.promise
})
})
})
describe('2 level tree', () => {
// 2 levels tree
// ┌◉┐
// │c│
// ┌◉─┘ └─◉┐
// │b d│
// ◉─┘ └─◉
// a
let psA, psB, psC, psD, psE
// Create and start pubsub nodes
beforeEach(async () => {
[psA, psB, psC, psD, psE] = await common.setup(5)
// Start pubsub nodes
;[psA, psB, psC, psD, psE].map((p) => p.start())
})
// connect nodes
beforeEach(async () => {
await psA._libp2p.dial(psB.peerId)
await psB._libp2p.dial(psC.peerId)
await psC._libp2p.dial(psD.peerId)
await psD._libp2p.dial(psE.peerId)
// Wait for peers to be ready in pubsub
await pWaitFor(() =>
psA.peers.size === 1 &&
psB.peers.size === 2 &&
psC.peers.size === 2 &&
psD.peers.size === 2 &&
psE.peers.size === 1
)
})
afterEach(async () => {
[psA, psB, psC, psD, psE].map((p) => p.stop())
await common.teardown()
})
it('subscribes', () => {
psA.subscribe('Z')
expectSet(psA.subscriptions, ['Z'])
psB.subscribe('Z')
expectSet(psB.subscriptions, ['Z'])
psC.subscribe('Z')
expectSet(psC.subscriptions, ['Z'])
psD.subscribe('Z')
expectSet(psD.subscriptions, ['Z'])
psE.subscribe('Z')
expectSet(psE.subscriptions, ['Z'])
})
it('publishes from c', async function () {
this.timeout(30 * 1000)
const defer = pDefer()
let counter = 0
psA.subscribe('Z')
psA.on('Z', incMsg)
psB.subscribe('Z')
psB.on('Z', incMsg)
psC.subscribe('Z')
psC.on('Z', incMsg)
psD.subscribe('Z')
psD.on('Z', incMsg)
psE.subscribe('Z')
psE.on('Z', incMsg)
await Promise.all([
new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psB.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psC.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psD.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psE.once('pubsub:subscription-change', resolve))
])
// await a cycle
await delay(1000)
psC.publish('Z', uint8ArrayFromString('hey from c'))
function incMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('hey from c')
check()
}
function check () {
if (++counter === 5) {
psA.unsubscribe('Z', incMsg)
psB.unsubscribe('Z', incMsg)
psC.unsubscribe('Z', incMsg)
psD.unsubscribe('Z', incMsg)
psE.unsubscribe('Z', incMsg)
defer.resolve()
}
}
return defer.promise
})
})
})
describe('only some nodes subscribe the networks', () => {
describe('line', () => {
// line
// ◉────◎────◉
// a b c
before(() => { })
after(() => { })
})
describe('1 level tree', () => {
// 1 level tree
// ┌◉┐
// │b│
// ◎─┘ └─◉
// a c
before(() => { })
after(() => { })
})
describe('2 level tree', () => {
// 2 levels tree
// ┌◉┐
// │c│
// ┌◎─┘ └─◉┐
// │b d│
// ◉─┘ └─◎
// a e
before(() => { })
after(() => { })
})
})
})
}

View File

@ -0,0 +1,229 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')
const {
first,
expectSet
} = require('./utils')
const topic = 'foo'
function shouldNotHappen (_) {
expect.fail()
}
module.exports = (common) => {
describe('pubsub with two nodes', () => {
describe('fresh nodes', () => {
let psA, psB
// Create pubsub nodes and connect them
before(async () => {
[psA, psB] = await common.setup(2)
expect(psA.peers.size).to.be.eql(0)
expect(psB.peers.size).to.be.eql(0)
// Start pubsub and connect nodes
psA.start()
psB.start()
await psA._libp2p.dial(psB.peerId)
// Wait for peers to be ready in pubsub
await pWaitFor(() => psA.peers.size === 1 && psB.peers.size === 1)
})
after(async () => {
sinon.restore()
psA && psA.stop()
psB && psB.stop()
await common.teardown()
})
it('Subscribe to a topic in nodeA', () => {
const defer = pDefer()
psB.once('pubsub:subscription-change', (changedPeerId, changedSubs) => {
expectSet(psA.subscriptions, [topic])
expect(psB.peers.size).to.equal(1)
expectSet(psB.topics.get(topic), [psA.peerId.toB58String()])
expect(changedPeerId.toB58String()).to.equal(first(psB.peers).id.toB58String())
expect(changedSubs).to.be.eql([{ topicID: topic, subscribe: true }])
defer.resolve()
})
psA.subscribe(topic)
return defer.promise
})
it('Publish to a topic in nodeA', () => {
const defer = pDefer()
psA.once(topic, (msg) => {
expect(uint8ArrayToString(msg.data)).to.equal('hey')
psB.removeListener(topic, shouldNotHappen)
defer.resolve()
})
psB.once(topic, shouldNotHappen)
psA.publish(topic, uint8ArrayFromString('hey'))
return defer.promise
})
it('Publish to a topic in nodeB', () => {
const defer = pDefer()
psA.once(topic, (msg) => {
psA.once(topic, shouldNotHappen)
expect(uint8ArrayToString(msg.data)).to.equal('banana')
setTimeout(() => {
psA.removeListener(topic, shouldNotHappen)
psB.removeListener(topic, shouldNotHappen)
defer.resolve()
}, 100)
})
psB.once(topic, shouldNotHappen)
psB.publish(topic, uint8ArrayFromString('banana'))
return defer.promise
})
it('Publish 10 msg to a topic in nodeB', () => {
const defer = pDefer()
let counter = 0
psB.once(topic, shouldNotHappen)
psA.on(topic, receivedMsg)
function receivedMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('banana')
expect(msg.from).to.be.eql(psB.peerId.toB58String())
expect(msg.seqno).to.be.a('Uint8Array')
expect(msg.topicIDs).to.be.eql([topic])
if (++counter === 10) {
psA.removeListener(topic, receivedMsg)
psB.removeListener(topic, shouldNotHappen)
defer.resolve()
}
}
Array.from({ length: 10 }, (_, i) => psB.publish(topic, uint8ArrayFromString('banana')))
return defer.promise
})
it('Unsubscribe from topic in nodeA', () => {
const defer = pDefer()
psA.unsubscribe(topic)
expect(psA.subscriptions.size).to.equal(0)
psB.once('pubsub:subscription-change', (changedPeerId, changedSubs) => {
expect(psB.peers.size).to.equal(1)
expectSet(psB.topics.get(topic), [])
expect(changedPeerId.toB58String()).to.equal(first(psB.peers).id.toB58String())
expect(changedSubs).to.be.eql([{ topicID: topic, subscribe: false }])
defer.resolve()
})
return defer.promise
})
it('Publish to a topic:Z in nodeA nodeB', () => {
const defer = pDefer()
psA.once('Z', shouldNotHappen)
psB.once('Z', shouldNotHappen)
setTimeout(() => {
psA.removeListener('Z', shouldNotHappen)
psB.removeListener('Z', shouldNotHappen)
defer.resolve()
}, 100)
psB.publish('Z', uint8ArrayFromString('banana'))
psA.publish('Z', uint8ArrayFromString('banana'))
return defer.promise
})
})
describe('nodes send state on connection', () => {
let psA, psB
// Create pubsub nodes and connect them
before(async () => {
[psA, psB] = await common.setup(2)
expect(psA.peers.size).to.be.eql(0)
expect(psB.peers.size).to.be.eql(0)
// Start pubsub and connect nodes
psA.start()
psB.start()
})
// Make subscriptions prior to nodes connected
before(() => {
psA.subscribe('Za')
psB.subscribe('Zb')
expect(psA.peers.size).to.equal(0)
expectSet(psA.subscriptions, ['Za'])
expect(psB.peers.size).to.equal(0)
expectSet(psB.subscriptions, ['Zb'])
})
after(async () => {
sinon.restore()
psA && psA.stop()
psB && psB.stop()
await common.teardown()
})
it('existing subscriptions are sent upon peer connection', async function () {
this.timeout(10e3)
await Promise.all([
psA._libp2p.dial(psB.peerId),
new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psB.once('pubsub:subscription-change', resolve))
])
expect(psA.peers.size).to.equal(1)
expect(psB.peers.size).to.equal(1)
expectSet(psA.subscriptions, ['Za'])
expectSet(psB.topics.get('Za'), [psA.peerId.toB58String()])
expectSet(psB.subscriptions, ['Zb'])
expectSet(psA.topics.get('Zb'), [psB.peerId.toB58String()])
})
})
})
}

10
src/pubsub/tests/utils.js Normal file
View File

@ -0,0 +1,10 @@
// @ts-nocheck interface tests
'use strict'
const { expect } = require('chai')
exports.first = (map) => map.values().next().value
exports.expectSet = (set, subs) => {
expect(Array.from(set.values())).to.eql(subs)
}

137
src/pubsub/utils.js Normal file
View File

@ -0,0 +1,137 @@
'use strict'
// @ts-ignore libp2p crypto has no types
const randomBytes = require('libp2p-crypto/src/random-bytes')
const uint8ArrayToString = require('uint8arrays/to-string')
const uint8ArrayFromString = require('uint8arrays/from-string')
const PeerId = require('peer-id')
const multihash = require('multihashes')
/**
* Generatea random sequence number.
*
* @returns {Uint8Array}
* @private
*/
const randomSeqno = () => {
return randomBytes(8)
}
/**
* Generate a message id, based on the `from` and `seqno`.
*
* @param {string} from
* @param {Uint8Array} seqno
* @returns {Uint8Array}
* @private
*/
const msgId = (from, seqno) => {
const fromBytes = PeerId.createFromB58String(from).id
const msgId = new Uint8Array(fromBytes.length + seqno.length)
msgId.set(fromBytes, 0)
msgId.set(seqno, fromBytes.length)
return msgId
}
/**
* Generate a message id, based on message `data`.
*
* @param {Uint8Array} data
* @returns {Uint8Array}
* @private
*/
const noSignMsgId = (data) => multihash.encode(data, 'sha2-256')
/**
* Check if any member of the first set is also a member
* of the second set.
*
* @param {Set<number>|Array<number>} a
* @param {Set<number>|Array<number>} b
* @returns {boolean}
* @private
*/
const anyMatch = (a, b) => {
let bHas
if (Array.isArray(b)) {
/**
* @param {number} val
*/
bHas = (val) => b.indexOf(val) > -1
} else {
/**
* @param {number} val
*/
bHas = (val) => b.has(val)
}
for (const val of a) {
if (bHas(val)) {
return true
}
}
return false
}
/**
* Make everything an array.
*
* @template T
* @param {T|T[]} maybeArray
* @returns {T[]}
* @private
*/
const ensureArray = (maybeArray) => {
if (!Array.isArray(maybeArray)) {
return [maybeArray]
}
return maybeArray
}
/**
* Ensures `message.from` is base58 encoded
*
* @template {{from?:any}} T
* @param {T & {from?:string, receivedFrom:string}} message
* @param {string} [peerId]
* @returns {T & {from?: string, peerId?: string }}
*/
const normalizeInRpcMessage = (message, peerId) => {
const m = Object.assign({}, message)
if (message.from instanceof Uint8Array) {
m.from = uint8ArrayToString(message.from, 'base58btc')
}
if (peerId) {
m.receivedFrom = peerId
}
return m
}
/**
* @template {{from?:any, data?:any}} T
*
* @param {T} message
* @returns {T & {from?: Uint8Array, data?: Uint8Array}}
*/
const normalizeOutRpcMessage = (message) => {
const m = Object.assign({}, message)
if (typeof message.from === 'string') {
m.from = uint8ArrayFromString(message.from, 'base58btc')
}
if (typeof message.data === 'string') {
m.data = uint8ArrayFromString(message.data)
}
return m
}
module.exports = {
randomSeqno,
msgId,
noSignMsgId,
anyMatch,
ensureArray,
normalizeInRpcMessage,
normalizeOutRpcMessage
}

91
src/record/README.md Normal file
View File

@ -0,0 +1,91 @@
interface-record
==================
A libp2p node needs to store data in a public location (e.g. a DHT), or rely on potentially untrustworthy intermediaries to relay information. Libp2p provides an all-purpose data container called **envelope**, which includes a signature of the data, so that it its authenticity can be verified.
The record represents the data that will be stored inside the **envelope** when distributing records across the network. The `interface-record` aims to guarantee that any type of record created is compliant with the libp2p **envelope**.
Taking into account that a record might be used in different contexts, an **envelope** signature made for a specific purpose **must not** be considered valid for a different purpose. Accordingly, each record has a short and descriptive string representing the record use case, known as **domain**. The data to be signed will be prepended with the domain string, in order to create a domain signature.
A record can also contain a Uint8Array codec (ideally registered as a [multicodec](https://github.com/multiformats/multicodec)). This codec will prefix the record data in the **envelope** , so that it can be deserialized deterministically.
## Usage
```js
const tests = require('libp2p-interfaces/src/record/tests')
describe('your record', () => {
tests({
async setup () {
return YourRecord
},
async teardown () {
// cleanup resources created by setup()
}
})
})
```
## Create Record
```js
const multicodec = require('multicodec')
const Record = require('libp2p-interfaces/src/record')
const fromString = require('uint8arrays/from-string')
// const Protobuf = require('./record.proto')
const ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record'
const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = fromString('0301', 'hex')
/**
* @implements {import('libp2p-interfaces/src/record/types').Record}
*/
class PeerRecord {
constructor (peerId, multiaddrs, seqNumber) {
this.domain = ENVELOPE_DOMAIN_PEER_RECORD
this.codec = ENVELOPE_PAYLOAD_TYPE_PEER_RECORD
}
/**
* Marshal a record to be used in an envelope.
*
* @returns {Uint8Array}
*/
marshal () {
// Implement and return using Protobuf
}
/**
* Returns true if `this` record equals the `other`.
*
* @param {PeerRecord} other
* @returns {other is Record}
*/
equals (other) {
// Verify
}
}
```
## API
### marshal
- `record.marshal()`
Marshal a record to be used in a libp2p envelope.
**Returns**
It returns a `Protobuf` containing the record data.
### equals
- `record.equals(other)`
Verifies if the other Record is identical to this one.
**Parameters**
- other is a `Record` to compare with the current instance.
**Returns**
- `other is Record`

36
src/record/tests/index.js Normal file
View File

@ -0,0 +1,36 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
module.exports = (test) => {
describe('record', () => {
let record
beforeEach(async () => {
record = await test.setup()
if (!record) throw new Error('missing record')
})
afterEach(() => test.teardown())
it('has domain and codec', () => {
expect(record.domain).to.exist()
expect(record.codec).to.exist()
})
it('is able to marshal', () => {
const rawData = record.marshal()
expect(rawData).to.be.an.instanceof(Uint8Array)
})
it('is able to compare two records', () => {
const equals = record.equals(record)
expect(equals).to.eql(true)
})
})
}

21
src/record/types.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
/**
* Record is the base implementation of a record that can be used as the payload of a libp2p envelope.
*/
export interface Record {
/**
* signature domain.
*/
domain: string;
/**
* identifier of the type of record
*/
codec: Uint8Array;
/**
* Marshal a record to be used in an envelope.
*/
marshal(): Uint8Array;
/**
* Verifies if the other provided Record is identical to this one.
*/
equals(other: unknown): boolean
}

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -5,7 +6,7 @@ const chai = require('chai')
chai.use(require('chai-checkmark'))
const { expect } = chai
const pair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const { collect, map, consume } = require('streaming-iterables')
function close (stream) {
@ -20,8 +21,9 @@ async function closeAndWait (stream) {
/**
* A tick is considered valid if it happened between now
* and `ms` milliseconds ago
* @param {number} date Time in ticks
* @param {number} ms max milliseconds that should have expired
*
* @param {number} date - Time in ticks
* @param {number} ms - max milliseconds that should have expired
* @returns {boolean}
*/
function isValidTick (date, ms = 5000) {

View File

@ -1,14 +1,16 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 8] */
'use strict'
const pair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const { consume } = require('streaming-iterables')
const Tcp = require('libp2p-tcp')
const multiaddr = require('multiaddr')
const abortable = require('abortable-iterator')
const AbortController = require('abort-controller')
const { source: abortable } = require('abortable-iterator')
const AbortController = require('abort-controller').default
const uint8arrayFromString = require('uint8arrays/from-string')
const mh = multiaddr('/ip4/127.0.0.1/tcp/0')
@ -17,7 +19,7 @@ function pause (ms) {
}
function randomBuffer () {
return Buffer.from(Math.random().toString())
return uint8arrayFromString(Math.random().toString())
}
const infiniteRandom = {

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,8 +1,10 @@
// @ts-nocheck interface tests
'use strict'
const { expect } = require('chai')
const pair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const pLimit = require('p-limit')
const { collect, tap, consume } = require('streaming-iterables')
@ -61,8 +63,11 @@ module.exports = async (Muxer, nStreams, nMsg, limit) => {
}
function marker (n) {
/** @type {Function} */
let check
let i = 0
/** @type {Promise<void>} */
const done = new Promise((resolve, reject) => {
check = err => {
i++
@ -78,5 +83,7 @@ function marker (n) {
}
}
})
// @ts-ignore - TS can't see that assignement occured
return { check, done }
}

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

51
src/stream-muxer/types.d.ts vendored Normal file
View File

@ -0,0 +1,51 @@
import BufferList from 'bl'
export interface MuxerFactory {
new (options: MuxerOptions): Muxer;
multicodec: string;
}
/**
* A libp2p stream muxer
*/
export interface Muxer {
readonly streams: Array<MuxedStream>;
/**
* Initiate a new stream with the given name. If no name is
* provided, the id of th stream will be used.
*/
newStream (name?: string): MuxedStream;
/**
* A function called when receiving a new stream from the remote.
*/
onStream (stream: MuxedStream): void;
/**
* A function called when a stream ends.
*/
onStreamEnd (stream: MuxedStream): void;
}
export type MuxerOptions = {
onStream: (stream: MuxedStream) => void;
onStreamEnd: (stream: MuxedStream) => void;
maxMsgSize?: number;
}
export type MuxedTimeline = {
open: number;
close?: number;
}
export interface MuxedStream extends AsyncIterable<Uint8Array | BufferList> {
close: () => void;
abort: () => void;
reset: () => void;
sink: Sink;
source: AsyncIterable<Uint8Array | BufferList>;
timeline: MuxedTimeline;
id: string;
}
export type Sink = (source: Uint8Array) => Promise<void>;

View File

@ -56,8 +56,8 @@ const toplogy = new MulticodecTopology({
max: 50,
multicodecs: ['/echo/1.0.0'],
handlers: {
onConnect: (peerInfo, conn) => {},
onDisconnect: (peerInfo) => {}
onConnect: (peerId, conn) => {},
onDisconnect: (peerId) => {}
}
})
```
@ -69,8 +69,8 @@ The `MulticodecTopology` extends the `Topology`, which makes the `Topology` API
### Topology
- `Topology`
- `peers<Map<string, PeerInfo>>`: A Map of peers belonging to the topology.
- `disconnect<function(PeerInfo)>`: Called when a peer has been disconnected
- `peers<Map<string, PeerId>>`: A Map of peers belonging to the topology.
- `disconnect<function(PeerId)>`: Called when a peer has been disconnected
#### Constructor
@ -79,8 +79,8 @@ const toplogy = new Topology({
min: 0,
max: 50,
handlers: {
onConnect: (peerInfo, conn) => {},
onDisconnect: (peerInfo) => {}
onConnect: (peerId, conn) => {},
onDisconnect: (peerId) => {}
}
})
```
@ -95,27 +95,27 @@ const toplogy = new Topology({
#### Set a peer
- `topology.peers.set(id, peerInfo)`
- `topology.peers.set(id, peerId)`
Add a peer to the topology.
**Parameters**
- `id` is the `string` that identifies the peer to add.
- `peerInfo` is the [PeerInfo][peer-info] of the peer to add.
- `peerId` is the [PeerId][peer-id] of the peer to add.
#### Notify about a peer disconnected event
- `topology.disconnect(peerInfo)`
- `topology.disconnect(peerId)`
**Parameters**
- `peerInfo` is the [PeerInfo][peer-info] of the peer disconnected.
- `peerId` is the [PeerId][peer-id] of the peer disconnected.
### Multicodec Topology
- `MulticodecTopology`
- `registrar<Registrar>`: The `Registrar` of the topology. This is set by the `Registrar` during registration.
- `peers<Map<string, PeerInfo>>`: The Map of peers that belong to the topology
- `disconnect<function(PeerInfo)>`: Disconnects a peer from the topology.
- `peers<Map<string, PeerId>>`: The Map of peers that belong to the topology
- `disconnect<function(PeerId)>`: Disconnects a peer from the topology.
#### Constructor
@ -125,8 +125,8 @@ const toplogy = new MulticodecTopology({
max: 50,
multicodecs: ['/echo/1.0.0'],
handlers: {
onConnect: (peerInfo, conn) => {},
onDisconnect: (peerInfo) => {}
onConnect: (peerId, conn) => {},
onDisconnect: (peerId) => {}
}
})
```
@ -139,3 +139,5 @@ const toplogy = new MulticodecTopology({
- `handlers` is an optional `Object` containing the handler called when a peer is connected or disconnected.
- `onConnect` is a `function` called everytime a peer is connected in the topology context.
- `onDisconnect` is a `function` called everytime a peer is disconnected in the topology context.
[peer-id]: https://github.com/libp2p/js-peer-id

View File

@ -1,18 +1,28 @@
'use strict'
const withIs = require('class-is')
const noop = () => {}
const topologySymbol = Symbol.for('@libp2p/js-interfaces/topology')
/**
* @typedef {import('peer-id')} PeerId
*/
/**
* @typedef {Object} Options
* @property {number} [min=0] - minimum needed connections.
* @property {number} [max=Infinity] - maximum needed connections.
* @property {Handlers} [handlers]
*
* @typedef {Object} Handlers
* @property {(peerId: PeerId, conn: Connection) => void} [onConnect] - protocol "onConnect" handler
* @property {(peerId: PeerId, error?:Error) => void} [onDisconnect] - protocol "onDisconnect" handler
*
* @typedef {import('../connection/connection')} Connection
*/
class Topology {
/**
* @param {Object} props
* @param {number} props.min minimum needed connections (default: 0)
* @param {number} props.max maximum needed connections (default: Infinity)
* @param {Object} [props.handlers]
* @param {function} [props.handlers.onConnect] protocol "onConnect" handler
* @param {function} [props.handlers.onDisconnect] protocol "onDisconnect" handler
* @constructor
* @param {Options} options
*/
constructor ({
min = 0,
@ -26,21 +36,48 @@ class Topology {
this._onConnect = handlers.onConnect || noop
this._onDisconnect = handlers.onDisconnect || noop
this.peers = new Map()
/**
* Set of peers that support the protocol.
*
* @type {Set<string>}
*/
this.peers = new Set()
}
set registrar (registrar) {
get [Symbol.toStringTag] () {
return 'Topology'
}
get [topologySymbol] () {
return true
}
/**
* Checks if the given value is a Topology instance.
*
* @param {any} other
* @returns {other is Topology}
*/
static isTopology (other) {
return Boolean(other && other[topologySymbol])
}
/**
* @param {any} registrar
*/
set registrar (registrar) { // eslint-disable-line
this._registrar = registrar
}
/**
* Notify about peer disconnected event.
* @param {PeerInfo} peerInfo
*
* @param {PeerId} peerId
* @returns {void}
*/
disconnect (peerInfo) {
this._onDisconnect(peerInfo)
disconnect (peerId) {
this._onDisconnect(peerId)
}
}
module.exports = withIs(Topology, { className: 'Topology', symbolName: '@libp2p/js-interfaces/topology' })
module.exports = Topology

View File

@ -1,20 +1,11 @@
'use strict'
const assert = require('assert')
const withIs = require('class-is')
const Topology = require('./index')
const multicodecTopologySymbol = Symbol.for('@libp2p/js-interfaces/topology/multicodec-topology')
class MulticodecTopology extends Topology {
/**
* @param {Object} props
* @param {number} props.min minimum needed connections (default: 0)
* @param {number} props.max maximum needed connections (default: Infinity)
* @param {Array<string>} props.multicodecs protocol multicodecs
* @param {Object} props.handlers
* @param {function} props.handlers.onConnect protocol "onConnect" handler
* @param {function} props.handlers.onDisconnect protocol "onDisconnect" handler
* @constructor
* @param {TopologyOptions & MulticodecOptions} props
*/
constructor ({
min,
@ -24,22 +15,54 @@ class MulticodecTopology extends Topology {
}) {
super({ min, max, handlers })
assert(multicodecs, 'one or more multicodec should be provided')
assert(handlers, 'the handlers should be provided')
assert(handlers.onConnect && typeof handlers.onConnect === 'function',
'the \'onConnect\' handler must be provided')
assert(handlers.onDisconnect && typeof handlers.onDisconnect === 'function',
'the \'onDisconnect\' handler must be provided')
if (!multicodecs) {
throw new Error('one or more multicodec should be provided')
}
if (!handlers) {
throw new Error('the handlers should be provided')
}
if (typeof handlers.onConnect !== 'function') {
throw new Error('the \'onConnect\' handler must be provided')
}
if (typeof handlers.onDisconnect !== 'function') {
throw new Error('the \'onDisconnect\' handler must be provided')
}
this.multicodecs = Array.isArray(multicodecs) ? multicodecs : [multicodecs]
this._registrar = undefined
this._onProtocolChange = this._onProtocolChange.bind(this)
this._onPeerConnect = this._onPeerConnect.bind(this)
}
set registrar (registrar) {
get [Symbol.toStringTag] () {
return 'Topology'
}
get [multicodecTopologySymbol] () {
return true
}
/**
* Checks if the given value is a `MulticodecTopology` instance.
*
* @param {any} other
* @returns {other is MulticodecTopology}
*/
static isMulticodecTopology (other) {
return Boolean(other && other[multicodecTopologySymbol])
}
/**
* @param {any} registrar
*/
set registrar (registrar) { // eslint-disable-line
this._registrar = registrar
this._registrar.peerStore.on('change:protocols', this._onProtocolChange)
this._registrar.connectionManager.on('peer:connect', this._onPeerConnect)
// Update topology peers
this._updatePeers(this._registrar.peerStore.peers.values())
@ -47,47 +70,81 @@ class MulticodecTopology extends Topology {
/**
* Update topology.
* @param {Array<PeerInfo>} peerInfoIterable
*
* @param {Array<{id: PeerId, multiaddrs: Array<Multiaddr>, protocols: Array<string>}>} peerDataIterable
* @returns {void}
*/
_updatePeers (peerInfoIterable) {
for (const peerInfo of peerInfoIterable) {
if (this.multicodecs.filter(multicodec => peerInfo.protocols.has(multicodec)).length) {
_updatePeers (peerDataIterable) {
for (const { id, protocols } of peerDataIterable) {
if (this.multicodecs.filter(multicodec => protocols.includes(multicodec)).length) {
// Add the peer regardless of whether or not there is currently a connection
this.peers.set(peerInfo.id.toB58String(), peerInfo)
this.peers.add(id.toB58String())
// If there is a connection, call _onConnect
const connection = this._registrar.getConnection(peerInfo)
connection && this._onConnect(peerInfo, connection)
const connection = this._registrar.getConnection(id)
connection && this._onConnect(id, connection)
} else {
// Remove any peers we might be tracking that are no longer of value to us
this.peers.delete(peerInfo.id.toB58String())
this.peers.delete(id.toB58String())
}
}
}
/**
* Check if a new peer support the multicodecs for this topology.
*
* @param {Object} props
* @param {PeerInfo} props.peerInfo
* @param {PeerId} props.peerId
* @param {Array<string>} props.protocols
*/
_onProtocolChange ({ peerInfo, protocols }) {
const existingPeer = this.peers.get(peerInfo.id.toB58String())
_onProtocolChange ({ peerId, protocols }) {
const hadPeer = this.peers.has(peerId.toB58String())
const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol))
// Not supporting the protocol anymore?
if (existingPeer && hasProtocol.length === 0) {
this._onDisconnect(peerInfo)
if (hadPeer && hasProtocol.length === 0) {
this._onDisconnect(peerId)
}
// New to protocol support
for (const protocol of protocols) {
if (this.multicodecs.includes(protocol)) {
this._updatePeers([peerInfo])
const peerData = this._registrar.peerStore.get(peerId)
this._updatePeers([peerData])
return
}
}
}
/**
* Verify if a new connected peer has a topology multicodec and call _onConnect.
*
* @param {Connection} connection
* @returns {void}
*/
_onPeerConnect (connection) {
// @ts-ignore - remotePeer does not existist on Connection
const peerId = connection.remotePeer
const protocols = this._registrar.peerStore.protoBook.get(peerId)
if (!protocols) {
return
}
if (this.multicodecs.find(multicodec => protocols.includes(multicodec))) {
this.peers.add(peerId.toB58String())
this._onConnect(peerId, connection)
}
}
}
module.exports = withIs(MulticodecTopology, { className: 'MulticodecTopology', symbolName: '@libp2p/js-interfaces/topology/multicodec-topology' })
/**
* @typedef {import('peer-id')} PeerId
* @typedef {import('multiaddr')} Multiaddr
* @typedef {import('../connection/connection')} Connection
* @typedef {import('.').Options} TopologyOptions
* @typedef {Object} MulticodecOptions
* @property {string[]} multicodecs - protocol multicodecs
* @property {Required<Handlers>} handlers
* @typedef {import('.').Handlers} Handlers
*/
module.exports = MulticodecTopology

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -8,19 +9,18 @@ chai.use(require('dirty-chai'))
const sinon = require('sinon')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const peers = require('../../utils/peers')
module.exports = (test) => {
describe('multicodec topology', () => {
let topology, peer
let topology, id
beforeEach(async () => {
topology = await test.setup()
if (!topology) throw new Error('missing multicodec topology')
const id = await PeerId.createFromJSON(peers[0])
peer = await PeerInfo.create(id)
id = await PeerId.createFromJSON(peers[0])
})
afterEach(async () => {
@ -38,7 +38,7 @@ module.exports = (test) => {
it('should trigger "onDisconnect" on peer disconnected', () => {
sinon.spy(topology, '_onDisconnect')
topology.disconnect(peer)
topology.disconnect(id)
expect(topology._onDisconnect.callCount).to.equal(1)
})
@ -47,13 +47,16 @@ module.exports = (test) => {
sinon.spy(topology, '_updatePeers')
expect(topology.peers.size).to.eql(0)
const id2 = await PeerId.createFromJSON(peers[1])
const peer2 = await PeerInfo.create(id2)
topology.multicodecs.forEach((m) => peer2.protocols.add(m))
const peerStore = topology._registrar.peerStore
const id2 = await PeerId.createFromJSON(peers[1])
peerStore.peers.set(id2.toB58String(), {
id: id2,
protocols: Array.from(topology.multicodecs)
})
peerStore.emit('change:protocols', {
peerInfo: peer2,
peerId: id2,
protocols: Array.from(topology.multicodecs)
})
@ -65,28 +68,67 @@ module.exports = (test) => {
sinon.spy(topology, '_onDisconnect')
expect(topology.peers.size).to.eql(0)
const id2 = await PeerId.createFromJSON(peers[1])
const peer2 = await PeerInfo.create(id2)
topology.multicodecs.forEach((m) => peer2.protocols.add(m))
const peerStore = topology._registrar.peerStore
const id2 = await PeerId.createFromJSON(peers[1])
peerStore.peers.set(id2.toB58String(), {
id: id2,
protocols: Array.from(topology.multicodecs)
})
peerStore.emit('change:protocols', {
peerInfo: peer2,
peerId: id2,
protocols: Array.from(topology.multicodecs)
})
expect(topology.peers.size).to.eql(1)
topology.multicodecs.forEach((m) => peer2.protocols.delete(m))
peerStore.peers.set(id2.toB58String(), {
id: id2,
protocols: []
})
// Peer does not support the protocol anymore
peerStore.emit('change:protocols', {
peerInfo: peer2,
peerId: id2,
protocols: []
})
expect(topology.peers.size).to.eql(1)
expect(topology._onDisconnect.callCount).to.equal(1)
expect(topology._onDisconnect.calledWith(peer2)).to.equal(true)
expect(topology._onDisconnect.calledWith(id2)).to.equal(true)
})
it('should trigger "onConnect" when a peer connects and has one of the topology multicodecs in its known protocols', () => {
sinon.spy(topology, '_onConnect')
sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns(topology.multicodecs)
topology._registrar.connectionManager.emit('peer:connect', {
remotePeer: id
})
expect(topology._onConnect.callCount).to.equal(1)
})
it('should not trigger "onConnect" when a peer connects and has none of the topology multicodecs in its known protocols', () => {
sinon.spy(topology, '_onConnect')
sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns([])
topology._registrar.connectionManager.emit('peer:connect', {
remotePeer: id
})
expect(topology._onConnect.callCount).to.equal(0)
})
it('should not trigger "onConnect" when a peer connects and its protocols are not known', () => {
sinon.spy(topology, '_onConnect')
sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns(undefined)
topology._registrar.connectionManager.emit('peer:connect', {
remotePeer: id
})
expect(topology._onConnect.callCount).to.equal(0)
})
})
}

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -8,19 +9,17 @@ chai.use(require('dirty-chai'))
const sinon = require('sinon')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const peers = require('../../utils/peers')
module.exports = (test) => {
describe('topology', () => {
let topology, peer
let topology, id
beforeEach(async () => {
topology = await test.setup()
if (!topology) throw new Error('missing multicodec topology')
const id = await PeerId.createFromJSON(peers[0])
peer = await PeerInfo.create(id)
id = await PeerId.createFromJSON(peers[0])
})
afterEach(async () => {
@ -38,7 +37,7 @@ module.exports = (test) => {
it('should trigger "onDisconnect" on peer disconnected', () => {
sinon.spy(topology, '_onDisconnect')
topology.disconnect(peer)
topology.disconnect(id)
expect(topology._onDisconnect.callCount).to.equal(1)
})

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -9,8 +10,8 @@ chai.use(dirtyChai)
const { isValidTick } = require('./utils')
const goodbye = require('it-goodbye')
const { collect } = require('streaming-iterables')
const pipe = require('it-pipe')
const AbortController = require('abort-controller')
const { pipe } = require('it-pipe')
const AbortController = require('abort-controller').default
const AbortError = require('../errors').AbortError
const sinon = require('sinon')

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint max-nested-callbacks: ["error", 8] */
/* eslint-env mocha */
'use strict'
@ -8,7 +9,9 @@ const expect = chai.expect
chai.use(dirtyChai)
const sinon = require('sinon')
const pipe = require('it-pipe')
const pWaitFor = require('p-wait-for')
const { pipe } = require('it-pipe')
const uint8arrayFromString = require('uint8arrays/from-string')
const { isValidTick } = require('./utils')
module.exports = (common) => {
@ -69,12 +72,12 @@ module.exports = (common) => {
])
// Give the listener a chance to finish its upgrade
await new Promise(resolve => setTimeout(resolve, 0))
await pWaitFor(() => listenerConns.length === 2)
// Wait for the data send and close to finish
await Promise.all([
pipe(
[Buffer.from('Some data that is never handled')],
[uint8arrayFromString('Some data that is never handled')],
socket1
),
// Closer the listener (will take a couple of seconds to time out)
@ -92,6 +95,23 @@ module.exports = (common) => {
expect(upgradeSpy.callCount).to.equal(2)
})
it('should not handle connection if upgradeInbound throws', async () => {
sinon.stub(upgrader, 'upgradeInbound').throws()
const listener = transport.createListener(() => {
throw new Error('should not handle the connection if upgradeInbound throws')
})
// Listen
await listener.listen(addrs[0])
// Create a connection to the listener
const socket = await transport.dial(addrs[0])
await pWaitFor(() => typeof socket.timeline.close === 'number')
await listener.close()
})
describe('events', () => {
it('connection', (done) => {
const upgradeSpy = sinon.spy(upgrader, 'upgradeInbound')

View File

@ -4,8 +4,9 @@ module.exports = {
/**
* A tick is considered valid if it happened between now
* and `ms` milliseconds ago
* @param {number} date Time in ticks
* @param {number} ms max milliseconds that should have expired
*
* @param {number} date - Time in ticks
* @param {number} ms - max milliseconds that should have expired
* @returns {boolean}
*/
isValidTick: function isValidTick (date, ms = 5000) {

72
src/transport/types.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
import BufferList from 'bl'
import events from 'events'
import { Multiaddr } from 'multiaddr'
import Connection from '../connection/connection'
import { Sink } from '../stream-muxer/types'
export interface TransportFactory<DialOptions extends { signal?: AbortSignal }, ListenerOptions> {
new(upgrader: Upgrader): Transport<DialOptions, ListenerOptions>;
}
/**
* A libp2p transport is understood as something that offers a dial and listen interface to establish connections.
*/
export interface Transport <DialOptions extends { signal?: AbortSignal }, ListenerOptions> {
/**
* Dial a given multiaddr.
*/
dial(ma: Multiaddr, options?: DialOptions): Promise<Connection>;
/**
* Create transport listeners.
*/
createListener(options: ListenerOptions, handler?: (connection: Connection) => void): Listener;
/**
* Takes a list of `Multiaddr`s and returns only valid addresses for the transport
*/
filter(multiaddrs: Multiaddr[]): Multiaddr[];
}
export interface Listener extends events.EventEmitter {
/**
* Start a listener
*/
listen(multiaddr: Multiaddr): Promise<void>;
/**
* Get listen addresses
*/
getAddrs(): Multiaddr[];
/**
* Close listener
*
* @returns {Promise<void>}
*/
close(): Promise<void>;
}
export interface Upgrader {
/**
* Upgrades an outbound connection on `transport.dial`.
*/
upgradeOutbound(maConn: MultiaddrConnection): Promise<Connection>;
/**
* Upgrades an inbound connection on transport listener.
*/
upgradeInbound(maConn: MultiaddrConnection): Promise<Connection>;
}
export type MultiaddrConnectionTimeline = {
open: number;
upgraded?: number;
close?: number;
}
export type MultiaddrConnection = {
sink: Sink;
source: AsyncIterable<Uint8Array | BufferList>;
close: (err?: Error) => Promise<void>;
conn: unknown;
remoteAddr: Multiaddr;
localAddr?: Multiaddr;
timeline: MultiaddrConnectionTimeline;
}

18
src/types.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
export interface EventEmitterFactory {
new(): EventEmitter;
}
export interface EventEmitter {
addListener(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
on(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
once(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
removeListener(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
off(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
removeAllListeners(event?: string | symbol): EventEmitter;
setMaxListeners(n: number): EventEmitter;
getMaxListeners(): number;
listeners(event: string | symbol): Function[]; // eslint-disable-line @typescript-eslint/ban-types
rawListeners(event: string | symbol): Function[]; // eslint-disable-line @typescript-eslint/ban-types
emit(event: string | symbol, ...args: any[]): boolean;
listenerCount(event: string | symbol): number;
}

View File

@ -13,11 +13,12 @@ describe('compliance tests', () => {
/**
* Test setup. `properties` allows the compliance test to override
* certain values for testing.
*
* @param {*} properties
*/
async setup (properties) {
const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080')
const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081')
const localAddr = new multiaddr.Multiaddr('/ip4/127.0.0.1/tcp/8080')
const remoteAddr = new multiaddr.Multiaddr('/ip4/127.0.0.1/tcp/8081')
const [localPeer, remotePeer] = await Promise.all([
PeerId.createFromJSON(peers[0]),
PeerId.createFromJSON(peers[1])
@ -25,7 +26,7 @@ describe('compliance tests', () => {
const openStreams = []
let streamId = 0
return new Connection({
const connection = new Connection({
localPeer,
remotePeer,
localAddr,
@ -43,7 +44,10 @@ describe('compliance tests', () => {
const id = streamId++
const stream = pair()
stream.close = () => stream.sink([])
stream.close = async () => {
await stream.sink([])
connection.removeStream(stream.id)
}
stream.id = id
openStreams.push(stream)
@ -57,6 +61,7 @@ describe('compliance tests', () => {
getStreams: () => openStreams,
...properties
})
return connection
},
async teardown () {
// cleanup resources created by setup()

View File

@ -0,0 +1,48 @@
/* eslint-env mocha */
'use strict'
const { Connection } = require('../../src/connection')
const peers = require('../../src/utils/peers')
const PeerId = require('peer-id')
const pair = require('it-pair')
describe('connection tests', () => {
it('should not require local or remote addrs', async () => {
const [localPeer, remotePeer] = await Promise.all([
PeerId.createFromJSON(peers[0]),
PeerId.createFromJSON(peers[1])
])
const openStreams = []
let streamId = 0
return new Connection({
localPeer,
remotePeer,
stat: {
timeline: {
open: Date.now() - 10,
upgraded: Date.now()
},
direction: 'outbound',
encryption: '/secio/1.0.0',
multiplexer: '/mplex/6.7.0'
},
newStream: (protocols) => {
const id = streamId++
const stream = pair()
stream.close = () => stream.sink([])
stream.id = id
openStreams.push(stream)
return {
stream,
protocol: protocols[0]
}
},
close: () => {},
getStreams: () => openStreams
})
})
})

View File

@ -5,13 +5,20 @@ const tests = require('../../src/peer-discovery/tests')
const MockDiscovery = require('./mock-discovery')
describe('compliance tests', () => {
let intervalId
tests({
async setup () {
await new Promise(resolve => setTimeout(resolve, 10))
return new MockDiscovery()
setup () {
const mockDiscovery = new MockDiscovery({
discoveryDelay: 1
})
intervalId = setInterval(mockDiscovery._discoverPeer, 1000)
return mockDiscovery
},
async teardown () {
await new Promise(resolve => setTimeout(resolve, 10))
teardown () {
clearInterval(intervalId)
}
})
})

View File

@ -2,8 +2,8 @@
const { EventEmitter } = require('events')
const multiaddr = require('multiaddr')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
/**
* Emits 'peer' events on discovery.
@ -37,10 +37,12 @@ class MockDiscovery extends EventEmitter {
if (!this._isRunning) return
const peerId = await PeerId.create({ bits: 512 })
const peerInfo = new PeerInfo(peerId)
this._timer = setTimeout(() => {
this.emit('peer', peerInfo)
this.emit('peer', {
id: peerId,
multiaddrs: [new multiaddr.Multiaddr('/ip4/127.0.0.1/tcp/8000')]
})
}, this.options.discoveryDelay || 1000)
}
}

View File

@ -0,0 +1,78 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const {
createPeerId,
mockRegistrar,
PubsubImplementation
} = require('./utils')
const uint8ArrayFromString = require('uint8arrays/from-string')
const protocol = '/pubsub/1.0.0'
const topic = 'foo'
const data = uint8ArrayFromString('bar')
const shouldNotHappen = (_) => expect.fail()
describe('emitSelf', () => {
let pubsub
describe('enabled', () => {
before(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId,
registrar: mockRegistrar
}, { emitSelf: true })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(() => {
pubsub.stop()
})
it('should emit to self on publish', () => {
const promise = new Promise((resolve) => pubsub.once(topic, resolve))
pubsub.publish(topic, data)
return promise
})
})
describe('disabled', () => {
before(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId,
registrar: mockRegistrar
}, { emitSelf: false })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(() => {
pubsub.stop()
})
it('should not emit to self on publish', () => {
pubsub.once(topic, (m) => shouldNotHappen)
pubsub.publish(topic, data)
// Wait 1 second to guarantee that self is not noticed
return new Promise((resolve) => setTimeout(() => resolve(), 1000))
})
})
})

View File

@ -0,0 +1,54 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const PubsubBaseImpl = require('../../src/pubsub')
const {
createPeerId,
mockRegistrar
} = require('./utils')
describe('pubsub instance', () => {
let peerId
before(async () => {
peerId = await createPeerId()
})
it('should throw if no debugName is provided', () => {
expect(() => {
new PubsubBaseImpl() // eslint-disable-line no-new
}).to.throw()
})
it('should throw if no multicodec is provided', () => {
expect(() => {
new PubsubBaseImpl({ // eslint-disable-line no-new
debugName: 'pubsub'
})
}).to.throw()
})
it('should throw if no libp2p is provided', () => {
expect(() => {
new PubsubBaseImpl({ // eslint-disable-line no-new
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0'
})
}).to.throw()
})
it('should accept valid parameters', () => {
expect(() => {
new PubsubBaseImpl({ // eslint-disable-line no-new
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0',
libp2p: {
peerId: peerId,
registrar: mockRegistrar
}
})
}).not.to.throw()
})
})

View File

@ -0,0 +1,227 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const PubsubBaseImpl = require('../../src/pubsub')
const {
createPeerId,
createMockRegistrar,
PubsubImplementation,
ConnectionPair
} = require('./utils')
describe('pubsub base lifecycle', () => {
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 PubsubBaseImpl({
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0',
libp2p: {
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 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, {
peerId: peerIdA,
registrar: createMockRegistrar(registrarRecordA)
})
pubsubB = new PubsubImplementation(protocol, {
peerId: peerIdB,
registrar: createMockRegistrar(registrarRecordB)
})
})
// start pubsub
beforeEach(() => {
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)
})
})
})

View File

@ -0,0 +1,90 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const PubsubBaseImpl = require('../../src/pubsub')
const { SignaturePolicy } = require('../../src/pubsub/signature-policy')
const {
createPeerId,
mockRegistrar
} = require('./utils')
describe('pubsub base messages', () => {
let peerId
let pubsub
before(async () => {
peerId = await createPeerId()
pubsub = new PubsubBaseImpl({
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0',
libp2p: {
peerId: peerId,
registrar: mockRegistrar
}
})
})
afterEach(() => {
sinon.restore()
})
it('_buildMessage normalizes and signs messages', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
const signedMessage = await pubsub._buildMessage(message)
expect(pubsub.validate(signedMessage)).to.not.be.rejected()
})
it('validate with StrictNoSign will reject a message with from, signature, key, seqno present', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictSign)
const signedMessage = await pubsub._buildMessage(message)
sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictNoSign)
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.from
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.signature
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.key
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.seqno
await expect(pubsub.validate(signedMessage)).to.not.be.rejected()
})
it('validate with StrictNoSign will validate a message without a signature, key, and seqno', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictNoSign)
const signedMessage = await pubsub._buildMessage(message)
expect(pubsub.validate(signedMessage)).to.not.be.rejected()
})
it('validate with StrictSign requires a signature', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
await expect(pubsub.validate(message)).to.be.rejectedWith(Error, 'Signing required and no signature was present')
})
})

358
test/pubsub/pubsub.spec.js Normal file
View File

@ -0,0 +1,358 @@
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const PeerStreams = require('../../src/pubsub/peer-streams')
const {
createPeerId,
createMockRegistrar,
ConnectionPair,
mockRegistrar,
PubsubImplementation
} = require('./utils')
const protocol = '/pubsub/1.0.0'
const topic = 'test-topic'
const message = uint8ArrayFromString('hello')
describe('pubsub base implementation', () => {
describe('publish', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
})
afterEach(() => pubsub.stop())
it('calls _publish for router to forward messages', async () => {
sinon.spy(pubsub, '_publish')
pubsub.start()
await pubsub.publish(topic, message)
expect(pubsub._publish.callCount).to.eql(1)
})
it('should sign messages on publish', async () => {
sinon.spy(pubsub, '_publish')
pubsub.start()
await pubsub.publish(topic, message)
// Get the first message sent to _publish, and validate it
const signedMessage = pubsub._publish.getCall(0).lastArg
try {
await pubsub.validate(signedMessage)
} catch (e) {
expect.fail('validation should not throw')
}
})
})
describe('subscribe', () => {
describe('basics', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
pubsub.start()
})
afterEach(() => 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, pubsubB
let peerIdA, peerIdB
const registrarRecordA = {}
const registrarRecordB = {}
beforeEach(async () => {
peerIdA = await createPeerId()
peerIdB = await createPeerId()
pubsubA = new PubsubImplementation(protocol, {
peerId: peerIdA,
registrar: createMockRegistrar(registrarRecordA)
})
pubsubB = new PubsubImplementation(protocol, {
peerId: peerIdB,
registrar: createMockRegistrar(registrarRecordB)
})
})
// start pubsub and connect nodes
beforeEach(async () => {
pubsubA.start()
pubsubB.start()
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
}
})
})
afterEach(() => {
pubsubA.stop()
pubsubB.stop()
})
it('should send subscribe message to connected peers', async () => {
sinon.spy(pubsubA, '_sendSubscriptions')
sinon.spy(pubsubB, '_processRpcSubOpt')
pubsubA.subscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(1)
// Other peer should receive subscription message
await pWaitFor(() => {
const subscribers = pubsubB.getSubscribers(topic)
return subscribers.length === 1
})
expect(pubsubB._processRpcSubOpt.callCount).to.eql(1)
})
})
})
describe('unsubscribe', () => {
describe('basics', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
pubsub.start()
})
afterEach(() => pubsub.stop())
it('should remove all subscriptions for a topic', () => {
pubsub.subscribe(topic, (msg) => {})
pubsub.subscribe(topic, (msg) => {})
expect(pubsub.subscriptions.size).to.eql(1)
pubsub.unsubscribe(topic)
expect(pubsub.subscriptions.size).to.eql(0)
})
})
describe('two nodes', () => {
let pubsubA, pubsubB
let peerIdA, peerIdB
const registrarRecordA = {}
const registrarRecordB = {}
beforeEach(async () => {
peerIdA = await createPeerId()
peerIdB = await createPeerId()
pubsubA = new PubsubImplementation(protocol, {
peerId: peerIdA,
registrar: createMockRegistrar(registrarRecordA)
})
pubsubB = new PubsubImplementation(protocol, {
peerId: peerIdB,
registrar: createMockRegistrar(registrarRecordB)
})
})
// start pubsub and connect nodes
beforeEach(async () => {
pubsubA.start()
pubsubB.start()
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
}
})
})
afterEach(() => {
pubsubA.stop()
pubsubB.stop()
})
it('should send unsubscribe message to connected peers', async () => {
sinon.spy(pubsubA, '_sendSubscriptions')
sinon.spy(pubsubB, '_processRpcSubOpt')
pubsubA.subscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(1)
// Other peer should receive subscription message
await pWaitFor(() => {
const subscribers = pubsubB.getSubscribers(topic)
return subscribers.length === 1
})
expect(pubsubB._processRpcSubOpt.callCount).to.eql(1)
// Unsubscribe
pubsubA.unsubscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(2)
// Other peer should receive subscription message
await pWaitFor(() => {
const subscribers = pubsubB.getSubscribers(topic)
return subscribers.length === 0
})
expect(pubsubB._processRpcSubOpt.callCount).to.eql(2)
})
it('should not send unsubscribe message to connected peers if not subscribed', () => {
sinon.spy(pubsubA, '_sendSubscriptions')
sinon.spy(pubsubB, '_processRpcSubOpt')
// Unsubscribe
pubsubA.unsubscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(0)
})
})
})
describe('getTopics', () => {
let peerId
let pubsub
beforeEach(async () => {
peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
pubsub.start()
})
afterEach(() => 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
let pubsub
beforeEach(async () => {
peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
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', () => {
// start pubsub
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', () => {
const topic = 'topic-test'
// start pubsub
pubsub.start()
let peersSubscribed = pubsub.getSubscribers(topic)
expect(peersSubscribed).to.be.empty()
// Set mock peer subscribed
const peer = new PeerStreams({ id: peerId })
const id = peer.id.toB58String()
pubsub.topics.set(topic, new Set([id]))
pubsub.peers.set(id, peer)
peersSubscribed = pubsub.getSubscribers(topic)
expect(peersSubscribed).to.not.be.empty()
expect(peersSubscribed[0]).to.eql(id)
})
})
})

93
test/pubsub/sign.spec.js Normal file
View File

@ -0,0 +1,93 @@
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 5] */
'use strict'
const { expect } = require('aegir/utils/chai')
const uint8ArrayConcat = require('uint8arrays/concat')
const uint8ArrayFromString = require('uint8arrays/from-string')
const { Message } = require('../../src/pubsub/message')
const {
signMessage,
SignPrefix,
verifySignature
} = require('../../src/pubsub/message/sign')
const PeerId = require('peer-id')
const { randomSeqno } = require('../../src/pubsub/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' })
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)
})
})

View File

@ -0,0 +1,104 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const pWaitFor = require('p-wait-for')
const errCode = require('err-code')
const PeerId = require('peer-id')
const uint8ArrayEquals = require('uint8arrays/equals')
const uint8ArrayFromString = require('uint8arrays/from-string')
const PeerStreams = require('../../src/pubsub/peer-streams')
const { SignaturePolicy } = require('../../src/pubsub/signature-policy')
const {
createPeerId,
mockRegistrar,
PubsubImplementation
} = require('./utils')
const protocol = '/pubsub/1.0.0'
describe('topic validators', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
}, {
globalSignaturePolicy: SignaturePolicy.StrictNoSign
})
pubsub.start()
})
afterEach(() => {
sinon.restore()
})
it('should filter messages by topic validator', async () => {
// use _publish.callCount() to see if a message is valid or not
sinon.spy(pubsub, '_publish')
sinon.stub(pubsub.peers, 'get').returns({})
const filteredTopic = 't'
const peer = new PeerStreams({ id: await PeerId.create() })
// Set a trivial topic validator
pubsub.topicValidators.set(filteredTopic, (topic, message) => {
if (!uint8ArrayEquals(message.data, uint8ArrayFromString('a message'))) {
throw errCode(new Error(), 'ERR_TOPIC_VALIDATOR_REJECT')
}
})
// valid case
const validRpc = {
subscriptions: [],
msgs: [{
data: uint8ArrayFromString('a message'),
topicIDs: [filteredTopic]
}]
}
// process valid message
pubsub.subscribe(filteredTopic)
pubsub._processRpc(peer.id.toB58String(), peer, validRpc)
await pWaitFor(() => pubsub._publish.callCount === 1)
// invalid case
const invalidRpc = {
subscriptions: [],
msgs: [{
data: uint8ArrayFromString('a different message'),
topicIDs: [filteredTopic]
}]
}
// process invalid message
pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc)
expect(pubsub._publish.callCount).to.eql(1)
// remove topic validator
pubsub.topicValidators.delete(filteredTopic)
// another invalid case
const invalidRpc2 = {
subscriptions: [],
msgs: [{
data: uint8ArrayFromString('a different message'),
topicIDs: [filteredTopic]
}]
}
// process previously invalid message, now is valid
pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc2)
pubsub.unsubscribe(filteredTopic)
await pWaitFor(() => pubsub._publish.callCount === 2)
})
})

78
test/pubsub/utils.spec.js Normal file
View File

@ -0,0 +1,78 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const utils = require('../../src/pubsub/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 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.deep.equal(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 }
]
for (let i = 0; i < m.length; i++) {
expect(utils.normalizeInRpcMessage(m[i])).to.deep.eql(expected[i])
}
})
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 }
]
for (let i = 0; i < m.length; i++) {
expect(utils.normalizeOutRpcMessage(m[i])).to.deep.eql(expected[i])
}
})
})

View File

@ -0,0 +1,85 @@
'use strict'
const DuplexPair = require('it-pair/duplex')
const PeerId = require('peer-id')
const PubsubBaseProtocol = require('../../../src/pubsub')
const { message } = require('../../../src/pubsub')
exports.createPeerId = async () => {
const peerId = await PeerId.create({ bits: 1024 })
return peerId
}
class PubsubImplementation extends PubsubBaseProtocol {
constructor (protocol, libp2p, options = {}) {
super({
debugName: 'libp2p:pubsub',
multicodecs: protocol,
libp2p,
...options
})
}
_publish (message) {
// ...
}
_decodeRpc (bytes) {
return message.rpc.RPC.decode(bytes)
}
_encodeRpc (rpc) {
return message.rpc.RPC.encode(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 })
}
]
}

View File

@ -6,6 +6,13 @@ class MockPeerStore extends EventEmitter {
constructor (peers) {
super()
this.peers = peers
this.protoBook = {
get: () => {}
}
}
get (peerId) {
return this.peers.get(peerId.toB58String())
}
}

View File

@ -1,6 +1,8 @@
/* eslint-env mocha */
'use strict'
const { EventEmitter } = require('events')
const tests = require('../../src/topology/tests/multicodec-topology')
const MulticodecTopology = require('../../src/topology/multicodec-topology')
const MockPeerStore = require('./mock-peer-store')
@ -21,10 +23,13 @@ describe('multicodec topology compliance tests', () => {
})
if (!registrar) {
const peerStore = new MockPeerStore([])
const peers = new Map()
const peerStore = new MockPeerStore(peers)
const connectionManager = new EventEmitter()
registrar = {
peerStore,
connectionManager,
getConnection: () => { }
}
}

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/aegir/src/config/tsconfig.aegir.json",
"compilerOptions": {
"outDir": "dist"
},
"include": [
"src"
]
}