├── .gitignore ├── LICENSE ├── .github ├── workflows │ ├── stale.yml │ ├── semantic-pull-request.yml │ ├── automerge.yml │ └── js-test-and-release.yml └── dependabot.yml ├── tsconfig.json ├── LICENSE-APACHE ├── LICENSE-MIT ├── test ├── message │ ├── rpc.proto │ └── rpc.ts ├── instance.spec.ts ├── emit-self.spec.ts ├── topic-validators.spec.ts ├── message.spec.ts ├── sign.spec.ts ├── utils │ └── index.ts ├── utils.spec.ts ├── lifecycle.spec.ts └── pubsub.spec.ts ├── src ├── errors.ts ├── sign.ts ├── utils.ts ├── peer-streams.ts └── index.ts ├── README.md ├── package.json └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .docs 5 | .coverage 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | .vscode 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is dual licensed under MIT and Apache-2.0. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/license-2.0 5 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close and mark stale issue 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | stale: 9 | uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src", 8 | "test" 9 | ], 10 | "exclude": [ 11 | "test/message/rpc.js" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "deps" 11 | prefix-development: "deps(dev)" 12 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3 13 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # File managed by web3-bot. DO NOT EDIT. 2 | # See https://github.com/protocol/.github/ for details. 3 | 4 | name: Automerge 5 | on: [ pull_request ] 6 | 7 | jobs: 8 | automerge: 9 | uses: protocol/.github/.github/workflows/automerge.yml@master 10 | with: 11 | job: 'automerge' 12 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/message/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message RPC { 4 | repeated SubOpts subscriptions = 1; 5 | repeated Message messages = 2; 6 | optional ControlMessage control = 3; 7 | 8 | message SubOpts { 9 | optional bool subscribe = 1; // subscribe or unsubcribe 10 | optional string topic = 2; 11 | } 12 | 13 | message Message { 14 | optional bytes from = 1; 15 | optional bytes data = 2; 16 | optional bytes seqno = 3; 17 | optional string topic = 4; 18 | optional bytes signature = 5; 19 | optional bytes key = 6; 20 | } 21 | } 22 | 23 | message ControlMessage { 24 | repeated ControlIHave ihave = 1; 25 | repeated ControlIWant iwant = 2; 26 | repeated ControlGraft graft = 3; 27 | repeated ControlPrune prune = 4; 28 | } 29 | 30 | message ControlIHave { 31 | optional string topic = 1; 32 | repeated bytes messageIDs = 2; 33 | } 34 | 35 | message ControlIWant { 36 | repeated bytes messageIDs = 1; 37 | } 38 | 39 | message ControlGraft { 40 | optional string topic = 1; 41 | } 42 | 43 | message ControlPrune { 44 | optional string topic = 1; 45 | repeated PeerInfo peers = 2; 46 | optional uint64 backoff = 3; 47 | } 48 | 49 | message PeerInfo { 50 | optional bytes peerID = 1; 51 | optional bytes signedPeerRecord = 2; 52 | } 53 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | 2 | export const codes = { 3 | /** 4 | * Signature policy is invalid 5 | */ 6 | ERR_INVALID_SIGNATURE_POLICY: 'ERR_INVALID_SIGNATURE_POLICY', 7 | /** 8 | * Signature policy is unhandled 9 | */ 10 | ERR_UNHANDLED_SIGNATURE_POLICY: 'ERR_UNHANDLED_SIGNATURE_POLICY', 11 | 12 | // Strict signing codes 13 | 14 | /** 15 | * Message expected to have a `signature`, but doesn't 16 | */ 17 | ERR_MISSING_SIGNATURE: 'ERR_MISSING_SIGNATURE', 18 | /** 19 | * Message expected to have a `seqno`, but doesn't 20 | */ 21 | ERR_MISSING_SEQNO: 'ERR_MISSING_SEQNO', 22 | /** 23 | * Message expected to have a `key`, but doesn't 24 | */ 25 | ERR_MISSING_KEY: 'ERR_MISSING_KEY', 26 | /** 27 | * Message `signature` is invalid 28 | */ 29 | ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE', 30 | /** 31 | * Message expected to have a `from`, but doesn't 32 | */ 33 | ERR_MISSING_FROM: 'ERR_MISSING_FROM', 34 | 35 | // Strict no-signing codes 36 | 37 | /** 38 | * Message expected to not have a `from`, but does 39 | */ 40 | ERR_UNEXPECTED_FROM: 'ERR_UNEXPECTED_FROM', 41 | /** 42 | * Message expected to not have a `signature`, but does 43 | */ 44 | ERR_UNEXPECTED_SIGNATURE: 'ERR_UNEXPECTED_SIGNATURE', 45 | /** 46 | * Message expected to not have a `key`, but does 47 | */ 48 | ERR_UNEXPECTED_KEY: 'ERR_UNEXPECTED_KEY', 49 | /** 50 | * Message expected to not have a `seqno`, but does 51 | */ 52 | ERR_UNEXPECTED_SEQNO: 'ERR_UNEXPECTED_SEQNO', 53 | 54 | /** 55 | * Message failed topic validator 56 | */ 57 | ERR_TOPIC_VALIDATOR_REJECT: 'ERR_TOPIC_VALIDATOR_REJECT' 58 | } 59 | -------------------------------------------------------------------------------- /test/instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 2 | import { expect } from 'aegir/chai' 3 | import { PubSubBaseProtocol } from '../src/index.js' 4 | import { MockRegistrar } from './utils/index.js' 5 | import type { PublishResult, PubSubRPC, PubSubRPCMessage } from '@libp2p/interface-pubsub' 6 | import type { Uint8ArrayList } from 'uint8arraylist' 7 | 8 | class PubsubProtocol extends PubSubBaseProtocol { 9 | decodeRpc (bytes: Uint8Array): PubSubRPC { 10 | throw new Error('Method not implemented.') 11 | } 12 | 13 | encodeRpc (rpc: PubSubRPC): Uint8Array { 14 | throw new Error('Method not implemented.') 15 | } 16 | 17 | decodeMessage (bytes: Uint8Array | Uint8ArrayList): PubSubRPCMessage { 18 | throw new Error('Method not implemented.') 19 | } 20 | 21 | encodeMessage (rpc: PubSubRPCMessage): Uint8Array { 22 | throw new Error('Method not implemented.') 23 | } 24 | 25 | async publishMessage (): Promise { 26 | throw new Error('Method not implemented.') 27 | } 28 | } 29 | 30 | describe('pubsub instance', () => { 31 | it('should throw if no init is provided', () => { 32 | expect(() => { 33 | // @ts-expect-error incorrect constructor args 34 | new PubsubProtocol() // eslint-disable-line no-new 35 | }).to.throw() 36 | }) 37 | 38 | it('should accept valid parameters', async () => { 39 | const peerId = await createEd25519PeerId() 40 | 41 | expect(() => { 42 | return new PubsubProtocol({ 43 | peerId, 44 | registrar: new MockRegistrar() 45 | }, { // eslint-disable-line no-new 46 | multicodecs: ['/pubsub/1.0.0'] 47 | }) 48 | }).not.to.throw() 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📁 Archived - this module has been merged into [js-libp2p](https://github.com/libp2p/js-libp2p/tree/master/packages/pubsub) 2 | 3 | # @libp2p/pubsub 4 | 5 | [![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) 6 | [![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) 7 | [![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-pubsub.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-pubsub) 8 | [![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p-pubsub/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p-pubsub/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) 9 | 10 | > libp2p pubsub base class 11 | 12 | ## Table of contents 13 | 14 | - [Install](#install) 15 | - [Browser ` 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```console 38 | npm i @libp2p/pubsub 39 | ``` 40 | 41 | ```javascript 42 | import { PubSubBaseProtocol } from '@libp2p/pubsub' 43 | 44 | class MyPubsubImplementation extends PubSubBaseProtocol { 45 | // .. extra methods here 46 | } 47 | ``` 48 | 49 | ## API Docs 50 | 51 | - 52 | 53 | ## License 54 | 55 | Licensed under either of 56 | 57 | - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) 58 | - MIT ([LICENSE-MIT](LICENSE-MIT) / ) 59 | 60 | ## Contribution 61 | 62 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 63 | -------------------------------------------------------------------------------- /test/emit-self.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'aegir/chai' 2 | import delay from 'delay' 3 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 4 | import { 5 | createPeerId, 6 | MockRegistrar, 7 | PubsubImplementation 8 | } from './utils/index.js' 9 | 10 | const protocol = '/pubsub/1.0.0' 11 | const topic = 'foo' 12 | const data = uint8ArrayFromString('bar') 13 | const shouldNotHappen = (): void => expect.fail() 14 | 15 | describe('emitSelf', () => { 16 | let pubsub: PubsubImplementation 17 | 18 | describe('enabled', () => { 19 | before(async () => { 20 | const peerId = await createPeerId() 21 | 22 | pubsub = new PubsubImplementation({ 23 | peerId, 24 | registrar: new MockRegistrar() 25 | }, { 26 | multicodecs: [protocol], 27 | emitSelf: true 28 | }) 29 | }) 30 | 31 | before(async () => { 32 | await pubsub.start() 33 | pubsub.subscribe(topic) 34 | }) 35 | 36 | after(async () => { 37 | await pubsub.stop() 38 | }) 39 | 40 | it('should emit to self on publish', async () => { 41 | pubsub.subscribe(topic) 42 | 43 | const promise = new Promise((resolve) => { 44 | pubsub.addEventListener('message', (evt) => { 45 | if (evt.detail.topic === topic) { 46 | resolve() 47 | } 48 | }) 49 | }) 50 | 51 | await pubsub.publish(topic, data) 52 | 53 | await promise 54 | }) 55 | 56 | it('should publish a message without data', async () => { 57 | pubsub.subscribe(topic) 58 | 59 | const promise = new Promise((resolve) => { 60 | pubsub.addEventListener('message', (evt) => { 61 | if (evt.detail.topic === topic) { 62 | resolve() 63 | } 64 | }) 65 | }) 66 | 67 | await pubsub.publish(topic) 68 | 69 | await promise 70 | }) 71 | }) 72 | 73 | describe('disabled', () => { 74 | before(async () => { 75 | const peerId = await createPeerId() 76 | 77 | pubsub = new PubsubImplementation({ 78 | peerId, 79 | registrar: new MockRegistrar() 80 | }, { 81 | multicodecs: [protocol], 82 | emitSelf: false 83 | }) 84 | }) 85 | 86 | before(async () => { 87 | await pubsub.start() 88 | pubsub.subscribe(topic) 89 | }) 90 | 91 | after(async () => { 92 | await pubsub.stop() 93 | }) 94 | 95 | it('should not emit to self on publish', async () => { 96 | pubsub.subscribe(topic) 97 | pubsub.addEventListener('message', shouldNotHappen) 98 | 99 | await pubsub.publish(topic, data) 100 | 101 | // Wait 1 second to guarantee that self is not noticed 102 | await delay(1000) 103 | }) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /test/topic-validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { type PubSubRPC, TopicValidatorResult } from '@libp2p/interface-pubsub' 2 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 3 | import { expect } from 'aegir/chai' 4 | import pWaitFor from 'p-wait-for' 5 | import sinon from 'sinon' 6 | import { equals as uint8ArrayEquals } from 'uint8arrays/equals' 7 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 8 | import { PeerStreams } from '../src/peer-streams.js' 9 | import { 10 | MockRegistrar, 11 | PubsubImplementation 12 | } from './utils/index.js' 13 | import type { PeerId } from '@libp2p/interface-peer-id' 14 | 15 | const protocol = '/pubsub/1.0.0' 16 | 17 | describe('topic validators', () => { 18 | let pubsub: PubsubImplementation 19 | let peerId: PeerId 20 | let otherPeerId: PeerId 21 | 22 | beforeEach(async () => { 23 | peerId = await createEd25519PeerId() 24 | otherPeerId = await createEd25519PeerId() 25 | 26 | pubsub = new PubsubImplementation({ 27 | peerId, 28 | registrar: new MockRegistrar() 29 | }, { 30 | multicodecs: [protocol], 31 | globalSignaturePolicy: 'StrictNoSign' 32 | }) 33 | 34 | await pubsub.start() 35 | }) 36 | 37 | afterEach(() => { 38 | sinon.restore() 39 | }) 40 | 41 | it('should filter messages by topic validator', async () => { 42 | // use publishMessage.callCount() to see if a message is valid or not 43 | sinon.spy(pubsub, 'publishMessage') 44 | // @ts-expect-error not all fields are implemented in return value 45 | sinon.stub(pubsub.peers, 'get').returns({}) 46 | const filteredTopic = 't' 47 | const peer = new PeerStreams({ id: otherPeerId, protocol: 'a-protocol' }) 48 | 49 | // Set a trivial topic validator 50 | pubsub.topicValidators.set(filteredTopic, async (_otherPeerId, message) => { 51 | if (!uint8ArrayEquals(message.data, uint8ArrayFromString('a message'))) { 52 | return TopicValidatorResult.Reject 53 | } 54 | return TopicValidatorResult.Accept 55 | }) 56 | 57 | // valid case 58 | const validRpc: PubSubRPC = { 59 | subscriptions: [], 60 | messages: [{ 61 | from: otherPeerId.multihash.bytes, 62 | data: uint8ArrayFromString('a message'), 63 | topic: filteredTopic 64 | }] 65 | } 66 | 67 | // process valid message 68 | pubsub.subscribe(filteredTopic) 69 | void pubsub.processRpc(peer.id, peer, validRpc) 70 | 71 | // @ts-expect-error .callCount is a property added by sinon 72 | await pWaitFor(() => pubsub.publishMessage.callCount === 1) 73 | 74 | // invalid case 75 | const invalidRpc = { 76 | subscriptions: [], 77 | messages: [{ 78 | data: uint8ArrayFromString('a different message'), 79 | topic: filteredTopic 80 | }] 81 | } 82 | 83 | void pubsub.processRpc(peer.id, peer, invalidRpc) 84 | 85 | // @ts-expect-error .callCount is a property added by sinon 86 | expect(pubsub.publishMessage.callCount).to.eql(1) 87 | 88 | // remove topic validator 89 | pubsub.topicValidators.delete(filteredTopic) 90 | 91 | // another invalid case 92 | const invalidRpc2: PubSubRPC = { 93 | subscriptions: [], 94 | messages: [{ 95 | from: otherPeerId.multihash.bytes, 96 | data: uint8ArrayFromString('a different message'), 97 | topic: filteredTopic 98 | }] 99 | } 100 | 101 | // process previously invalid message, now is valid 102 | void pubsub.processRpc(peer.id, peer, invalidRpc2) 103 | pubsub.unsubscribe(filteredTopic) 104 | 105 | // @ts-expect-error .callCount is a property added by sinon 106 | await pWaitFor(() => pubsub.publishMessage.callCount === 2) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /test/message.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { expect } from 'aegir/chai' 3 | import sinon from 'sinon' 4 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 5 | import { randomSeqno } from '../src/utils.js' 6 | import { 7 | createPeerId, 8 | MockRegistrar, 9 | PubsubImplementation 10 | } from './utils/index.js' 11 | import type { PeerId } from '@libp2p/interface-peer-id' 12 | import type { Message } from '@libp2p/interface-pubsub' 13 | 14 | describe('pubsub base messages', () => { 15 | let peerId: PeerId 16 | let pubsub: PubsubImplementation 17 | 18 | before(async () => { 19 | peerId = await createPeerId() 20 | pubsub = new PubsubImplementation({ 21 | peerId, 22 | registrar: new MockRegistrar() 23 | }, { 24 | multicodecs: ['/pubsub/1.0.0'] 25 | }) 26 | }) 27 | 28 | afterEach(() => { 29 | sinon.restore() 30 | }) 31 | 32 | it('buildMessage normalizes and signs messages', async () => { 33 | const message = { 34 | from: peerId, 35 | data: uint8ArrayFromString('hello'), 36 | topic: 'test-topic', 37 | sequenceNumber: randomSeqno() 38 | } 39 | 40 | const signedMessage = await pubsub.buildMessage(message) 41 | 42 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.not.be.rejected() 43 | }) 44 | 45 | it('validate with StrictNoSign will reject a message with from, signature, key, seqno present', async () => { 46 | const message = { 47 | from: peerId, 48 | data: uint8ArrayFromString('hello'), 49 | topic: 'test-topic', 50 | sequenceNumber: randomSeqno() 51 | } 52 | 53 | sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictSign') 54 | 55 | const signedMessage = await pubsub.buildMessage(message) 56 | 57 | if (signedMessage.type === 'unsigned') { 58 | throw new Error('Message was not signed') 59 | } 60 | 61 | sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign') 62 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.be.rejected() 63 | // @ts-expect-error this field is not optional 64 | delete signedMessage.from 65 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.be.rejected() 66 | // @ts-expect-error this field is not optional 67 | delete signedMessage.signature 68 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.be.rejected() 69 | // @ts-expect-error this field is not optional 70 | delete signedMessage.key 71 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.be.rejected() 72 | // @ts-expect-error this field is not optional 73 | delete signedMessage.sequenceNumber 74 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.not.be.rejected() 75 | }) 76 | 77 | it('validate with StrictNoSign will validate a message without a signature, key, and seqno', async () => { 78 | const message = { 79 | from: peerId, 80 | data: uint8ArrayFromString('hello'), 81 | topic: 'test-topic', 82 | sequenceNumber: randomSeqno() 83 | } 84 | 85 | sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign') 86 | 87 | const signedMessage = await pubsub.buildMessage(message) 88 | await expect(pubsub.validate(peerId, signedMessage)).to.eventually.not.be.rejected() 89 | }) 90 | 91 | it('validate with StrictSign requires a signature', async () => { 92 | // @ts-expect-error incomplete implementation 93 | const message: Message = { 94 | type: 'signed', 95 | data: uint8ArrayFromString('hello'), 96 | topic: 'test-topic' 97 | } 98 | 99 | await expect(pubsub.validate(peerId, message)).to.be.rejectedWith(Error, 'Signing required and no signature was present') 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /src/sign.ts: -------------------------------------------------------------------------------- 1 | import { keys } from '@libp2p/crypto' 2 | import { peerIdFromKeys } from '@libp2p/peer-id' 3 | import { concat as uint8ArrayConcat } from 'uint8arrays/concat' 4 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 5 | import { toRpcMessage } from './utils.js' 6 | import type { PeerId } from '@libp2p/interface-peer-id' 7 | import type { PubSubRPCMessage, SignedMessage } from '@libp2p/interface-pubsub' 8 | 9 | export const SignPrefix = uint8ArrayFromString('libp2p-pubsub:') 10 | 11 | /** 12 | * Signs the provided message with the given `peerId` 13 | */ 14 | export async function signMessage (peerId: PeerId, message: { from: PeerId, topic: string, data: Uint8Array, sequenceNumber: bigint }, encode: (rpc: PubSubRPCMessage) => Uint8Array): Promise { 15 | if (peerId.privateKey == null) { 16 | throw new Error('Cannot sign message, no private key present') 17 | } 18 | 19 | if (peerId.publicKey == null) { 20 | throw new Error('Cannot sign message, no public key present') 21 | } 22 | 23 | // @ts-expect-error signature field is missing, added below 24 | const outputMessage: SignedMessage = { 25 | type: 'signed', 26 | topic: message.topic, 27 | data: message.data, 28 | sequenceNumber: message.sequenceNumber, 29 | from: peerId 30 | } 31 | 32 | // Get the message in bytes, and prepend with the pubsub prefix 33 | const bytes = uint8ArrayConcat([ 34 | SignPrefix, 35 | encode(toRpcMessage(outputMessage)).subarray() 36 | ]) 37 | 38 | const privateKey = await keys.unmarshalPrivateKey(peerId.privateKey) 39 | outputMessage.signature = await privateKey.sign(bytes) 40 | outputMessage.key = peerId.publicKey 41 | 42 | return outputMessage 43 | } 44 | 45 | /** 46 | * Verifies the signature of the given message 47 | */ 48 | export async function verifySignature (message: SignedMessage, encode: (rpc: PubSubRPCMessage) => Uint8Array): Promise { 49 | if (message.type !== 'signed') { 50 | throw new Error('Message type must be "signed" to be verified') 51 | } 52 | 53 | if (message.signature == null) { 54 | throw new Error('Message must contain a signature to be verified') 55 | } 56 | 57 | if (message.from == null) { 58 | throw new Error('Message must contain a from property to be verified') 59 | } 60 | 61 | // Get message sans the signature 62 | const bytes = uint8ArrayConcat([ 63 | SignPrefix, 64 | encode({ 65 | ...toRpcMessage(message), 66 | signature: undefined, 67 | key: undefined 68 | }).subarray() 69 | ]) 70 | 71 | // Get the public key 72 | const pubKeyBytes = await messagePublicKey(message) 73 | const pubKey = keys.unmarshalPublicKey(pubKeyBytes) 74 | 75 | // verify the base message 76 | return pubKey.verify(bytes, message.signature) 77 | } 78 | 79 | /** 80 | * Returns the PublicKey associated with the given message. 81 | * If no valid PublicKey can be retrieved an error will be returned. 82 | */ 83 | export async function messagePublicKey (message: SignedMessage): Promise { 84 | if (message.type !== 'signed') { 85 | throw new Error('Message type must be "signed" to have a public key') 86 | } 87 | 88 | // should be available in the from property of the message (peer id) 89 | if (message.from == null) { 90 | throw new Error('Could not get the public key from the originator id') 91 | } 92 | 93 | if (message.key != null) { 94 | const keyPeerId = await peerIdFromKeys(message.key) 95 | 96 | if (keyPeerId.publicKey != null) { 97 | return keyPeerId.publicKey 98 | } 99 | } 100 | 101 | if (message.from.publicKey != null) { 102 | return message.from.publicKey 103 | } 104 | 105 | // We couldn't validate pubkey is from the originator, error 106 | throw new Error('Could not get the public key from the originator id') 107 | } 108 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from '@libp2p/crypto' 2 | import { CodeError } from '@libp2p/interfaces/errors' 3 | import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' 4 | import { sha256 } from 'multiformats/hashes/sha2' 5 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 6 | import { toString as uint8ArrayToString } from 'uint8arrays/to-string' 7 | import { codes } from './errors.js' 8 | import type { Message, PubSubRPCMessage } from '@libp2p/interface-pubsub' 9 | 10 | /** 11 | * Generate a random sequence number 12 | */ 13 | export function randomSeqno (): bigint { 14 | return BigInt(`0x${uint8ArrayToString(randomBytes(8), 'base16')}`) 15 | } 16 | 17 | /** 18 | * Generate a message id, based on the `key` and `seqno` 19 | */ 20 | export const msgId = (key: Uint8Array, seqno: bigint): Uint8Array => { 21 | const seqnoBytes = uint8ArrayFromString(seqno.toString(16).padStart(16, '0'), 'base16') 22 | 23 | const msgId = new Uint8Array(key.length + seqnoBytes.length) 24 | msgId.set(key, 0) 25 | msgId.set(seqnoBytes, key.length) 26 | 27 | return msgId 28 | } 29 | 30 | /** 31 | * Generate a message id, based on message `data` 32 | */ 33 | export const noSignMsgId = (data: Uint8Array): Uint8Array | Promise => { 34 | return sha256.encode(data) 35 | } 36 | 37 | /** 38 | * Check if any member of the first set is also a member 39 | * of the second set 40 | */ 41 | export const anyMatch = (a: Set | number[], b: Set | number[]): boolean => { 42 | let bHas 43 | if (Array.isArray(b)) { 44 | bHas = (val: number) => b.includes(val) 45 | } else { 46 | bHas = (val: number) => b.has(val) 47 | } 48 | 49 | for (const val of a) { 50 | if (bHas(val)) { 51 | return true 52 | } 53 | } 54 | 55 | return false 56 | } 57 | 58 | /** 59 | * Make everything an array 60 | */ 61 | export const ensureArray = function (maybeArray: T | T[]): T[] { 62 | if (!Array.isArray(maybeArray)) { 63 | return [maybeArray] 64 | } 65 | 66 | return maybeArray 67 | } 68 | 69 | const isSigned = async (message: PubSubRPCMessage): Promise => { 70 | if ((message.sequenceNumber == null) || (message.from == null) || (message.signature == null)) { 71 | return false 72 | } 73 | // if a public key is present in the `from` field, the message should be signed 74 | const fromID = peerIdFromBytes(message.from) 75 | if (fromID.publicKey != null) { 76 | return true 77 | } 78 | 79 | if (message.key != null) { 80 | const signingID = await peerIdFromKeys(message.key) 81 | return signingID.equals(fromID) 82 | } 83 | 84 | return false 85 | } 86 | 87 | export const toMessage = async (message: PubSubRPCMessage): Promise => { 88 | if (message.from == null) { 89 | throw new CodeError('RPC message was missing from', codes.ERR_MISSING_FROM) 90 | } 91 | 92 | if (!await isSigned(message)) { 93 | return { 94 | type: 'unsigned', 95 | topic: message.topic ?? '', 96 | data: message.data ?? new Uint8Array(0) 97 | } 98 | } 99 | 100 | const from = peerIdFromBytes(message.from) 101 | 102 | const msg: Message = { 103 | type: 'signed', 104 | from: peerIdFromBytes(message.from), 105 | topic: message.topic ?? '', 106 | sequenceNumber: bigIntFromBytes(message.sequenceNumber ?? new Uint8Array(0)), 107 | data: message.data ?? new Uint8Array(0), 108 | signature: message.signature ?? new Uint8Array(0), 109 | key: message.key ?? from.publicKey ?? new Uint8Array(0) 110 | } 111 | 112 | if (msg.key.length === 0) { 113 | throw new CodeError('Signed RPC message was missing key', codes.ERR_MISSING_KEY) 114 | } 115 | 116 | return msg 117 | } 118 | 119 | export const toRpcMessage = (message: Message): PubSubRPCMessage => { 120 | if (message.type === 'signed') { 121 | return { 122 | from: message.from.multihash.bytes, 123 | data: message.data, 124 | sequenceNumber: bigIntToBytes(message.sequenceNumber), 125 | topic: message.topic, 126 | signature: message.signature, 127 | key: message.key 128 | } 129 | } 130 | 131 | return { 132 | data: message.data, 133 | topic: message.topic 134 | } 135 | } 136 | 137 | export const bigIntToBytes = (num: bigint): Uint8Array => { 138 | let str = num.toString(16) 139 | 140 | if (str.length % 2 !== 0) { 141 | str = `0${str}` 142 | } 143 | 144 | return uint8ArrayFromString(str, 'base16') 145 | } 146 | 147 | export const bigIntFromBytes = (num: Uint8Array): bigint => { 148 | return BigInt(`0x${uint8ArrayToString(num, 'base16')}`) 149 | } 150 | -------------------------------------------------------------------------------- /test/sign.spec.ts: -------------------------------------------------------------------------------- 1 | import { keys } from '@libp2p/crypto' 2 | import * as PeerIdFactory from '@libp2p/peer-id-factory' 3 | import { expect } from 'aegir/chai' 4 | import { concat as uint8ArrayConcat } from 'uint8arrays/concat' 5 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 6 | import { 7 | signMessage, 8 | SignPrefix, 9 | verifySignature 10 | } from '../src/sign.js' 11 | import { randomSeqno, toRpcMessage } from '../src/utils.js' 12 | import { RPC } from './message/rpc.js' 13 | import type { PeerId } from '@libp2p/interface-peer-id' 14 | import type { PubSubRPCMessage } from '@libp2p/interface-pubsub' 15 | 16 | function encodeMessage (message: PubSubRPCMessage): Uint8Array { 17 | return RPC.Message.encode(message) 18 | } 19 | 20 | describe('message signing', () => { 21 | let peerId: PeerId 22 | 23 | before(async () => { 24 | peerId = await PeerIdFactory.createRSAPeerId({ 25 | bits: 1024 26 | }) 27 | }) 28 | 29 | it('should be able to sign and verify a message', async () => { 30 | const message = { 31 | type: 'signed', 32 | from: peerId, 33 | data: uint8ArrayFromString('hello'), 34 | sequenceNumber: randomSeqno(), 35 | topic: 'test-topic' 36 | } 37 | 38 | // @ts-expect-error missing fields 39 | const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(message)).subarray()]) 40 | 41 | if (peerId.privateKey == null) { 42 | throw new Error('No private key found on PeerId') 43 | } 44 | 45 | const privateKey = await keys.unmarshalPrivateKey(peerId.privateKey) 46 | const expectedSignature = await privateKey.sign(bytesToSign) 47 | 48 | const signedMessage = await signMessage(peerId, message, encodeMessage) 49 | 50 | // Check the signature and public key 51 | expect(signedMessage.signature).to.equalBytes(expectedSignature) 52 | expect(signedMessage.key).to.equalBytes(peerId.publicKey) 53 | 54 | // Verify the signature 55 | const verified = await verifySignature({ 56 | ...signedMessage, 57 | from: peerId 58 | }, encodeMessage) 59 | expect(verified).to.eql(true) 60 | }) 61 | 62 | it('should be able to extract the public key from an inlined key', async () => { 63 | const secPeerId = await PeerIdFactory.createSecp256k1PeerId() 64 | 65 | const message = { 66 | type: 'signed', 67 | from: secPeerId, 68 | data: uint8ArrayFromString('hello'), 69 | sequenceNumber: randomSeqno(), 70 | topic: 'test-topic' 71 | } 72 | 73 | // @ts-expect-error missing fields 74 | const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(message)).subarray()]) 75 | 76 | if (secPeerId.privateKey == null) { 77 | throw new Error('No private key found on PeerId') 78 | } 79 | 80 | const privateKey = await keys.unmarshalPrivateKey(secPeerId.privateKey) 81 | const expectedSignature = await privateKey.sign(bytesToSign) 82 | 83 | const signedMessage = await signMessage(secPeerId, message, encodeMessage) 84 | 85 | // Check the signature and public key 86 | expect(signedMessage.signature).to.eql(expectedSignature) 87 | // @ts-expect-error field is required 88 | signedMessage.key = undefined 89 | 90 | // Verify the signature 91 | const verified = await verifySignature({ 92 | ...signedMessage, 93 | from: secPeerId 94 | }, encodeMessage) 95 | expect(verified).to.eql(true) 96 | }) 97 | 98 | it('should be able to extract the public key from the message', async () => { 99 | const message = { 100 | type: 'signed', 101 | from: peerId, 102 | data: uint8ArrayFromString('hello'), 103 | sequenceNumber: randomSeqno(), 104 | topic: 'test-topic' 105 | } 106 | 107 | // @ts-expect-error missing fields 108 | const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(message)).subarray()]) 109 | 110 | if (peerId.privateKey == null) { 111 | throw new Error('No private key found on PeerId') 112 | } 113 | 114 | const privateKey = await keys.unmarshalPrivateKey(peerId.privateKey) 115 | const expectedSignature = await privateKey.sign(bytesToSign) 116 | 117 | const signedMessage = await signMessage(peerId, message, encodeMessage) 118 | 119 | // Check the signature and public key 120 | expect(signedMessage.signature).to.equalBytes(expectedSignature) 121 | expect(signedMessage.key).to.equalBytes(peerId.publicKey) 122 | 123 | // Verify the signature 124 | const verified = await verifySignature({ 125 | ...signedMessage, 126 | from: peerId 127 | }, encodeMessage) 128 | expect(verified).to.eql(true) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as PeerIdFactory from '@libp2p/peer-id-factory' 2 | import { duplexPair } from 'it-pair/duplex' 3 | import { PubSubBaseProtocol } from '../../src/index.js' 4 | import { RPC } from '../message/rpc.js' 5 | import type { Connection } from '@libp2p/interface-connection' 6 | import type { PeerId } from '@libp2p/interface-peer-id' 7 | import type { PublishResult, PubSubRPC, PubSubRPCMessage } from '@libp2p/interface-pubsub' 8 | import type { IncomingStreamData, Registrar, StreamHandler, StreamHandlerRecord, Topology } from '@libp2p/interface-registrar' 9 | 10 | export const createPeerId = async (): Promise => { 11 | const peerId = await PeerIdFactory.createEd25519PeerId() 12 | 13 | return peerId 14 | } 15 | 16 | export class PubsubImplementation extends PubSubBaseProtocol { 17 | async publishMessage (): Promise { 18 | return { 19 | recipients: [] 20 | } 21 | } 22 | 23 | decodeRpc (bytes: Uint8Array): PubSubRPC { 24 | return RPC.decode(bytes) 25 | } 26 | 27 | encodeRpc (rpc: PubSubRPC): Uint8Array { 28 | return RPC.encode(rpc) 29 | } 30 | 31 | decodeMessage (bytes: Uint8Array): PubSubRPCMessage { 32 | return RPC.Message.decode(bytes) 33 | } 34 | 35 | encodeMessage (rpc: PubSubRPCMessage): Uint8Array { 36 | return RPC.Message.encode(rpc) 37 | } 38 | } 39 | 40 | export class MockRegistrar implements Registrar { 41 | private readonly topologies = new Map() 42 | private readonly handlers = new Map() 43 | 44 | getProtocols (): string[] { 45 | const protocols = new Set() 46 | 47 | for (const topology of this.topologies.values()) { 48 | topology.protocols.forEach(protocol => protocols.add(protocol)) 49 | } 50 | 51 | for (const protocol of this.handlers.keys()) { 52 | protocols.add(protocol) 53 | } 54 | 55 | return Array.from(protocols).sort() 56 | } 57 | 58 | async handle (protocols: string | string[], handler: StreamHandler): Promise { 59 | const protocolList = Array.isArray(protocols) ? protocols : [protocols] 60 | 61 | for (const protocol of protocolList) { 62 | if (this.handlers.has(protocol)) { 63 | throw new Error(`Handler already registered for protocol ${protocol}`) 64 | } 65 | 66 | this.handlers.set(protocol, handler) 67 | } 68 | } 69 | 70 | async unhandle (protocols: string | string[]): Promise { 71 | const protocolList = Array.isArray(protocols) ? protocols : [protocols] 72 | 73 | protocolList.forEach(protocol => { 74 | this.handlers.delete(protocol) 75 | }) 76 | } 77 | 78 | getHandler (protocol: string): StreamHandlerRecord { 79 | const handler = this.handlers.get(protocol) 80 | 81 | if (handler == null) { 82 | throw new Error(`No handler registered for protocol ${protocol}`) 83 | } 84 | 85 | return { handler, options: {} } 86 | } 87 | 88 | async register (protocols: string | string[], topology: Topology): Promise { 89 | if (!Array.isArray(protocols)) { 90 | protocols = [protocols] 91 | } 92 | 93 | const id = `topology-id-${Math.random()}` 94 | 95 | this.topologies.set(id, { 96 | topology, 97 | protocols 98 | }) 99 | 100 | return id 101 | } 102 | 103 | unregister (id: string | string[]): void { 104 | if (!Array.isArray(id)) { 105 | id = [id] 106 | } 107 | 108 | id.forEach(id => this.topologies.delete(id)) 109 | } 110 | 111 | getTopologies (protocol: string): Topology[] { 112 | const output: Topology[] = [] 113 | 114 | for (const { topology, protocols } of this.topologies.values()) { 115 | if (protocols.includes(protocol)) { 116 | output.push(topology) 117 | } 118 | } 119 | 120 | if (output.length > 0) { 121 | return output 122 | } 123 | 124 | throw new Error(`No topologies registered for protocol ${protocol}`) 125 | } 126 | } 127 | 128 | export const ConnectionPair = (): [Connection, Connection] => { 129 | const [d0, d1] = duplexPair() 130 | 131 | return [ 132 | { 133 | // @ts-expect-error incomplete implementation 134 | newStream: async (protocol: string[]) => Promise.resolve({ 135 | ...d0, 136 | stat: { 137 | protocol: protocol[0] 138 | } 139 | }) 140 | }, 141 | { 142 | // @ts-expect-error incomplete implementation 143 | newStream: async (protocol: string[]) => Promise.resolve({ 144 | ...d1, 145 | stat: { 146 | protocol: protocol[0] 147 | } 148 | }) 149 | } 150 | ] 151 | } 152 | 153 | export async function mockIncomingStreamEvent (protocol: string, conn: Connection, remotePeer: PeerId): Promise { 154 | return { 155 | stream: await conn.newStream([protocol]), 156 | // @ts-expect-error incomplete implementation 157 | connection: { 158 | remotePeer 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { peerIdFromBytes, peerIdFromString } from '@libp2p/peer-id' 2 | import * as PeerIdFactory from '@libp2p/peer-id-factory' 3 | import { expect } from 'aegir/chai' 4 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 5 | import * as utils from '../src/utils.js' 6 | import type { Message, PubSubRPCMessage } from '@libp2p/interface-pubsub' 7 | 8 | describe('utils', () => { 9 | it('randomSeqno', () => { 10 | const first = utils.randomSeqno() 11 | const second = utils.randomSeqno() 12 | 13 | expect(first).to.be.a('BigInt') 14 | expect(second).to.be.a('BigInt') 15 | expect(first).to.not.equal(second) 16 | }) 17 | 18 | it('msgId should not generate same ID for two different Uint8Arrays', () => { 19 | const peerId = peerIdFromString('QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22') 20 | const msgId0 = utils.msgId(peerId.multihash.bytes, 1n) 21 | const msgId1 = utils.msgId(peerId.multihash.bytes, 2n) 22 | expect(msgId0).to.not.deep.equal(msgId1) 23 | }) 24 | 25 | it('anyMatch', () => { 26 | [ 27 | { a: [1, 2, 3], b: [4, 5, 6], result: false }, 28 | { a: [1, 2], b: [1, 2], result: true }, 29 | { a: [1, 2, 3], b: [4, 5, 1], result: true }, 30 | { a: [5, 6, 1], b: [1, 2, 3], result: true }, 31 | { a: [], b: [], result: false }, 32 | { a: [1], b: [2], result: false } 33 | ].forEach((test) => { 34 | expect(utils.anyMatch(new Set(test.a), new Set(test.b))).to.equal(test.result) 35 | expect(utils.anyMatch(new Set(test.a), test.b)).to.equal(test.result) 36 | }) 37 | }) 38 | 39 | it('ensureArray', () => { 40 | expect(utils.ensureArray('hello')).to.be.eql(['hello']) 41 | expect(utils.ensureArray([1, 2])).to.be.eql([1, 2]) 42 | }) 43 | 44 | it('converts an OUT msg.from to binary', () => { 45 | const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') 46 | const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' 47 | const m: Message[] = [{ 48 | type: 'signed', 49 | from: peerIdFromBytes(binaryId), 50 | topic: '', 51 | data: new Uint8Array(), 52 | sequenceNumber: 1n, 53 | signature: new Uint8Array(), 54 | key: new Uint8Array() 55 | }, { 56 | type: 'signed', 57 | from: peerIdFromString(stringId), 58 | topic: '', 59 | data: new Uint8Array(), 60 | sequenceNumber: 1n, 61 | signature: new Uint8Array(), 62 | key: new Uint8Array() 63 | }] 64 | const expected: PubSubRPCMessage[] = [{ 65 | from: binaryId, 66 | topic: '', 67 | data: new Uint8Array(), 68 | sequenceNumber: utils.bigIntToBytes(1n), 69 | signature: new Uint8Array(), 70 | key: new Uint8Array() 71 | }, { 72 | from: binaryId, 73 | topic: '', 74 | data: new Uint8Array(), 75 | sequenceNumber: utils.bigIntToBytes(1n), 76 | signature: new Uint8Array(), 77 | key: new Uint8Array() 78 | }] 79 | for (let i = 0; i < m.length; i++) { 80 | expect(utils.toRpcMessage(m[i])).to.deep.equal(expected[i]) 81 | } 82 | }) 83 | 84 | it('converts non-negative BigInts to bytes and back', () => { 85 | expect(utils.bigIntFromBytes(utils.bigIntToBytes(1n))).to.equal(1n) 86 | 87 | const values = [ 88 | 0n, 89 | 1n, 90 | 100n, 91 | 192832190818383818719287373223131n 92 | ] 93 | 94 | values.forEach(val => { 95 | expect(utils.bigIntFromBytes(utils.bigIntToBytes(val))).to.equal(val) 96 | }) 97 | }) 98 | 99 | it('ensures message is signed if public key is extractable', async () => { 100 | const dummyPeerID = await PeerIdFactory.createRSAPeerId() 101 | 102 | const cases: PubSubRPCMessage[] = [ 103 | { 104 | from: (await PeerIdFactory.createSecp256k1PeerId()).toBytes(), 105 | topic: 'test', 106 | data: new Uint8Array(0), 107 | sequenceNumber: utils.bigIntToBytes(1n), 108 | signature: new Uint8Array(0) 109 | }, 110 | { 111 | from: peerIdFromString('QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22').toBytes(), 112 | topic: 'test', 113 | data: new Uint8Array(0), 114 | sequenceNumber: utils.bigIntToBytes(1n), 115 | signature: new Uint8Array(0) 116 | }, 117 | { 118 | from: dummyPeerID.toBytes(), 119 | topic: 'test', 120 | data: new Uint8Array(0), 121 | sequenceNumber: utils.bigIntToBytes(1n), 122 | signature: new Uint8Array(0), 123 | key: dummyPeerID.publicKey 124 | }, 125 | { 126 | from: (await PeerIdFactory.createEd25519PeerId()).toBytes(), 127 | topic: 'test', 128 | data: new Uint8Array(0), 129 | sequenceNumber: utils.bigIntToBytes(1n), 130 | signature: new Uint8Array(0) 131 | } 132 | ] 133 | 134 | const expected = ['signed', 'unsigned', 'signed', 'signed'] 135 | const actual = (await Promise.all(cases.map(utils.toMessage))).map(m => m.type) 136 | 137 | expect(actual).to.deep.equal(expected) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /src/peer-streams.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' 2 | import { logger } from '@libp2p/logger' 3 | import { abortableSource } from 'abortable-iterator' 4 | import * as lp from 'it-length-prefixed' 5 | import { pipe } from 'it-pipe' 6 | import { pushable } from 'it-pushable' 7 | import { Uint8ArrayList } from 'uint8arraylist' 8 | import type { Stream } from '@libp2p/interface-connection' 9 | import type { PeerId } from '@libp2p/interface-peer-id' 10 | import type { PeerStreamEvents } from '@libp2p/interface-pubsub' 11 | import type { Pushable } from 'it-pushable' 12 | 13 | const log = logger('libp2p-pubsub:peer-streams') 14 | 15 | export interface PeerStreamsInit { 16 | id: PeerId 17 | protocol: string 18 | } 19 | 20 | /** 21 | * Thin wrapper around a peer's inbound / outbound pubsub streams 22 | */ 23 | export class PeerStreams extends EventEmitter { 24 | public readonly id: PeerId 25 | public readonly protocol: string 26 | /** 27 | * Write stream - it's preferable to use the write method 28 | */ 29 | public outboundStream?: Pushable 30 | /** 31 | * Read stream 32 | */ 33 | public inboundStream?: AsyncIterable 34 | /** 35 | * The raw outbound stream, as retrieved from conn.newStream 36 | */ 37 | private _rawOutboundStream?: Stream 38 | /** 39 | * The raw inbound stream, as retrieved from the callback from libp2p.handle 40 | */ 41 | private _rawInboundStream?: Stream 42 | /** 43 | * An AbortController for controlled shutdown of the inbound stream 44 | */ 45 | private readonly _inboundAbortController: AbortController 46 | private closed: boolean 47 | 48 | constructor (init: PeerStreamsInit) { 49 | super() 50 | 51 | this.id = init.id 52 | this.protocol = init.protocol 53 | 54 | this._inboundAbortController = new AbortController() 55 | this.closed = false 56 | } 57 | 58 | /** 59 | * Do we have a connection to read from? 60 | */ 61 | get isReadable (): boolean { 62 | return Boolean(this.inboundStream) 63 | } 64 | 65 | /** 66 | * Do we have a connection to write on? 67 | */ 68 | get isWritable (): boolean { 69 | return Boolean(this.outboundStream) 70 | } 71 | 72 | /** 73 | * Send a message to this peer. 74 | * Throws if there is no `stream` to write to available. 75 | */ 76 | write (data: Uint8Array | Uint8ArrayList): void { 77 | if (this.outboundStream == null) { 78 | const id = this.id.toString() 79 | throw new Error('No writable connection to ' + id) 80 | } 81 | 82 | this.outboundStream.push(data instanceof Uint8Array ? new Uint8ArrayList(data) : data) 83 | } 84 | 85 | /** 86 | * Attach a raw inbound stream and setup a read stream 87 | */ 88 | attachInboundStream (stream: Stream): AsyncIterable { 89 | // Create and attach a new inbound stream 90 | // The inbound stream is: 91 | // - abortable, set to only return on abort, rather than throw 92 | // - transformed with length-prefix transform 93 | this._rawInboundStream = stream 94 | this.inboundStream = abortableSource( 95 | pipe( 96 | this._rawInboundStream, 97 | (source) => lp.decode(source) 98 | ), 99 | this._inboundAbortController.signal, 100 | { returnOnAbort: true } 101 | ) 102 | 103 | this.dispatchEvent(new CustomEvent('stream:inbound')) 104 | return this.inboundStream 105 | } 106 | 107 | /** 108 | * Attach a raw outbound stream and setup a write stream 109 | */ 110 | async attachOutboundStream (stream: Stream): Promise> { 111 | // If an outbound stream already exists, gently close it 112 | const _prevStream = this.outboundStream 113 | if (this.outboundStream != null) { 114 | // End the stream without emitting a close event 115 | this.outboundStream.end() 116 | } 117 | 118 | this._rawOutboundStream = stream 119 | this.outboundStream = pushable({ 120 | objectMode: true, 121 | onEnd: (shouldEmit) => { 122 | // close writable side of the stream 123 | if (this._rawOutboundStream != null && this._rawOutboundStream.reset != null) { // eslint-disable-line @typescript-eslint/prefer-optional-chain 124 | this._rawOutboundStream.reset() 125 | } 126 | 127 | this._rawOutboundStream = undefined 128 | this.outboundStream = undefined 129 | if (shouldEmit != null) { 130 | this.dispatchEvent(new CustomEvent('close')) 131 | } 132 | } 133 | }) 134 | 135 | pipe( 136 | this.outboundStream, 137 | (source) => lp.encode(source), 138 | this._rawOutboundStream 139 | ).catch((err: Error) => { 140 | log.error(err) 141 | }) 142 | 143 | // Only emit if the connection is new 144 | if (_prevStream == null) { 145 | this.dispatchEvent(new CustomEvent('stream:outbound')) 146 | } 147 | 148 | return this.outboundStream 149 | } 150 | 151 | /** 152 | * Closes the open connection to peer 153 | */ 154 | close (): void { 155 | if (this.closed) { 156 | return 157 | } 158 | 159 | this.closed = true 160 | 161 | // End the outbound stream 162 | if (this.outboundStream != null) { 163 | this.outboundStream.end() 164 | } 165 | // End the inbound stream 166 | if (this.inboundStream != null) { 167 | this._inboundAbortController.abort() 168 | } 169 | 170 | this._rawOutboundStream = undefined 171 | this.outboundStream = undefined 172 | this._rawInboundStream = undefined 173 | this.inboundStream = undefined 174 | this.dispatchEvent(new CustomEvent('close')) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@libp2p/pubsub", 3 | "version": "7.0.3", 4 | "description": "libp2p pubsub base class", 5 | "license": "Apache-2.0 OR MIT", 6 | "homepage": "https://github.com/libp2p/js-libp2p-pubsub#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/libp2p/js-libp2p-pubsub.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/libp2p/js-libp2p-pubsub/issues" 13 | }, 14 | "keywords": [ 15 | "interface", 16 | "libp2p" 17 | ], 18 | "engines": { 19 | "node": ">=16.0.0", 20 | "npm": ">=7.0.0" 21 | }, 22 | "type": "module", 23 | "types": "./dist/src/index.d.ts", 24 | "typesVersions": { 25 | "*": { 26 | "*": [ 27 | "*", 28 | "dist/*", 29 | "dist/src/*", 30 | "dist/src/*/index" 31 | ], 32 | "src/*": [ 33 | "*", 34 | "dist/*", 35 | "dist/src/*", 36 | "dist/src/*/index" 37 | ] 38 | } 39 | }, 40 | "files": [ 41 | "src", 42 | "dist", 43 | "!dist/test", 44 | "!**/*.tsbuildinfo" 45 | ], 46 | "exports": { 47 | ".": { 48 | "types": "./dist/src/index.d.ts", 49 | "import": "./dist/src/index.js" 50 | }, 51 | "./errors": { 52 | "types": "./dist/src/errors.d.ts", 53 | "import": "./dist/src/errors.js" 54 | }, 55 | "./peer-streams": { 56 | "types": "./dist/src/peer-streams.d.ts", 57 | "import": "./dist/src/peer-streams.js" 58 | }, 59 | "./signature-policy": { 60 | "types": "./dist/src/signature-policy.d.ts", 61 | "import": "./dist/src/signature-policy.js" 62 | }, 63 | "./utils": { 64 | "types": "./dist/src/utils.d.ts", 65 | "import": "./dist/src/utils.js" 66 | } 67 | }, 68 | "eslintConfig": { 69 | "extends": "ipfs", 70 | "parserOptions": { 71 | "sourceType": "module" 72 | }, 73 | "ignorePatterns": [ 74 | "test/message/*.d.ts", 75 | "test/message/*.js" 76 | ] 77 | }, 78 | "release": { 79 | "branches": [ 80 | "master" 81 | ], 82 | "plugins": [ 83 | [ 84 | "@semantic-release/commit-analyzer", 85 | { 86 | "preset": "conventionalcommits", 87 | "releaseRules": [ 88 | { 89 | "breaking": true, 90 | "release": "major" 91 | }, 92 | { 93 | "revert": true, 94 | "release": "patch" 95 | }, 96 | { 97 | "type": "feat", 98 | "release": "minor" 99 | }, 100 | { 101 | "type": "fix", 102 | "release": "patch" 103 | }, 104 | { 105 | "type": "docs", 106 | "release": "patch" 107 | }, 108 | { 109 | "type": "test", 110 | "release": "patch" 111 | }, 112 | { 113 | "type": "deps", 114 | "release": "patch" 115 | }, 116 | { 117 | "scope": "no-release", 118 | "release": false 119 | } 120 | ] 121 | } 122 | ], 123 | [ 124 | "@semantic-release/release-notes-generator", 125 | { 126 | "preset": "conventionalcommits", 127 | "presetConfig": { 128 | "types": [ 129 | { 130 | "type": "feat", 131 | "section": "Features" 132 | }, 133 | { 134 | "type": "fix", 135 | "section": "Bug Fixes" 136 | }, 137 | { 138 | "type": "chore", 139 | "section": "Trivial Changes" 140 | }, 141 | { 142 | "type": "docs", 143 | "section": "Documentation" 144 | }, 145 | { 146 | "type": "deps", 147 | "section": "Dependencies" 148 | }, 149 | { 150 | "type": "test", 151 | "section": "Tests" 152 | } 153 | ] 154 | } 155 | } 156 | ], 157 | "@semantic-release/changelog", 158 | "@semantic-release/npm", 159 | "@semantic-release/github", 160 | "@semantic-release/git" 161 | ] 162 | }, 163 | "scripts": { 164 | "clean": "aegir clean", 165 | "lint": "aegir lint", 166 | "dep-check": "aegir dep-check -i protons", 167 | "build": "aegir build", 168 | "generate": "protons test/message/rpc.proto", 169 | "test": "aegir test", 170 | "test:chrome": "aegir test -t browser --cov", 171 | "test:chrome-webworker": "aegir test -t webworker", 172 | "test:firefox": "aegir test -t browser -- --browser firefox", 173 | "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", 174 | "test:node": "aegir test -t node --cov", 175 | "test:electron-main": "aegir test -t electron-main", 176 | "release": "aegir release", 177 | "docs": "aegir docs" 178 | }, 179 | "dependencies": { 180 | "@libp2p/crypto": "^1.0.0", 181 | "@libp2p/interface-connection": "^5.0.1", 182 | "@libp2p/interface-peer-id": "^2.0.1", 183 | "@libp2p/interface-pubsub": "^4.0.0", 184 | "@libp2p/interface-registrar": "^2.0.11", 185 | "@libp2p/interfaces": "^3.3.1", 186 | "@libp2p/logger": "^2.0.7", 187 | "@libp2p/peer-collections": "^3.0.1", 188 | "@libp2p/peer-id": "^2.0.3", 189 | "@libp2p/topology": "^4.0.1", 190 | "abortable-iterator": "^5.0.1", 191 | "it-length-prefixed": "^9.0.0", 192 | "it-pipe": "^3.0.1", 193 | "it-pushable": "^3.1.3", 194 | "multiformats": "^11.0.0", 195 | "p-queue": "^7.2.0", 196 | "uint8arraylist": "^2.0.0", 197 | "uint8arrays": "^4.0.2" 198 | }, 199 | "devDependencies": { 200 | "@libp2p/peer-id-factory": "^2.0.3", 201 | "@types/sinon": "^10.0.15", 202 | "aegir": "^39.0.10", 203 | "delay": "^6.0.0", 204 | "it-pair": "^2.0.6", 205 | "p-defer": "^4.0.0", 206 | "p-wait-for": "^5.0.0", 207 | "protons": "^7.0.2", 208 | "protons-runtime": "^5.0.0", 209 | "sinon": "^15.0.1" 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | # File managed by web3-bot. DO NOT EDIT. 2 | # See https://github.com/protocol/.github/ for details. 3 | 4 | name: test & maybe release 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | 11 | jobs: 12 | 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: lts/* 20 | - uses: ipfs/aegir/actions/cache-node-modules@master 21 | - run: npm run --if-present lint 22 | - run: npm run --if-present dep-check 23 | 24 | test-node: 25 | needs: check 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | os: [windows-latest, ubuntu-latest, macos-latest] 30 | node: [lts/*] 31 | fail-fast: true 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node }} 37 | - uses: ipfs/aegir/actions/cache-node-modules@master 38 | - run: npm run --if-present test:node 39 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 40 | with: 41 | flags: node 42 | 43 | test-chrome: 44 | needs: check 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: lts/* 51 | - uses: ipfs/aegir/actions/cache-node-modules@master 52 | - run: npm run --if-present test:chrome 53 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 54 | with: 55 | flags: chrome 56 | 57 | test-chrome-webworker: 58 | needs: check 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v3 62 | - uses: actions/setup-node@v3 63 | with: 64 | node-version: lts/* 65 | - uses: ipfs/aegir/actions/cache-node-modules@master 66 | - run: npm run --if-present test:chrome-webworker 67 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 68 | with: 69 | flags: chrome-webworker 70 | 71 | test-firefox: 72 | needs: check 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v3 76 | - uses: actions/setup-node@v3 77 | with: 78 | node-version: lts/* 79 | - uses: ipfs/aegir/actions/cache-node-modules@master 80 | - run: npm run --if-present test:firefox 81 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 82 | with: 83 | flags: firefox 84 | 85 | test-firefox-webworker: 86 | needs: check 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v3 90 | - uses: actions/setup-node@v3 91 | with: 92 | node-version: lts/* 93 | - uses: ipfs/aegir/actions/cache-node-modules@master 94 | - run: npm run --if-present test:firefox-webworker 95 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 96 | with: 97 | flags: firefox-webworker 98 | 99 | test-webkit: 100 | needs: check 101 | runs-on: ${{ matrix.os }} 102 | strategy: 103 | matrix: 104 | os: [ubuntu-latest, macos-latest] 105 | node: [lts/*] 106 | fail-fast: true 107 | steps: 108 | - uses: actions/checkout@v3 109 | - uses: actions/setup-node@v3 110 | with: 111 | node-version: lts/* 112 | - uses: ipfs/aegir/actions/cache-node-modules@master 113 | - run: npm run --if-present test:webkit 114 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 115 | with: 116 | flags: webkit 117 | 118 | test-webkit-webworker: 119 | needs: check 120 | runs-on: ${{ matrix.os }} 121 | strategy: 122 | matrix: 123 | os: [ubuntu-latest, macos-latest] 124 | node: [lts/*] 125 | fail-fast: true 126 | steps: 127 | - uses: actions/checkout@v3 128 | - uses: actions/setup-node@v3 129 | with: 130 | node-version: lts/* 131 | - uses: ipfs/aegir/actions/cache-node-modules@master 132 | - run: npm run --if-present test:webkit-webworker 133 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 134 | with: 135 | flags: webkit-webworker 136 | 137 | test-electron-main: 138 | needs: check 139 | runs-on: ubuntu-latest 140 | steps: 141 | - uses: actions/checkout@v3 142 | - uses: actions/setup-node@v3 143 | with: 144 | node-version: lts/* 145 | - uses: ipfs/aegir/actions/cache-node-modules@master 146 | - run: npx xvfb-maybe npm run --if-present test:electron-main 147 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 148 | with: 149 | flags: electron-main 150 | 151 | test-electron-renderer: 152 | needs: check 153 | runs-on: ubuntu-latest 154 | steps: 155 | - uses: actions/checkout@v3 156 | - uses: actions/setup-node@v3 157 | with: 158 | node-version: lts/* 159 | - uses: ipfs/aegir/actions/cache-node-modules@master 160 | - run: npx xvfb-maybe npm run --if-present test:electron-renderer 161 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 162 | with: 163 | flags: electron-renderer 164 | 165 | release: 166 | needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-webkit, test-webkit-webworker, test-electron-main, test-electron-renderer] 167 | runs-on: ubuntu-latest 168 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 169 | steps: 170 | - uses: actions/checkout@v3 171 | with: 172 | fetch-depth: 0 173 | - uses: actions/setup-node@v3 174 | with: 175 | node-version: lts/* 176 | - uses: ipfs/aegir/actions/cache-node-modules@master 177 | - uses: ipfs/aegir/actions/docker-login@master 178 | with: 179 | docker-token: ${{ secrets.DOCKER_TOKEN }} 180 | docker-username: ${{ secrets.DOCKER_USERNAME }} 181 | - run: npm run --if-present release 182 | env: 183 | GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN || github.token }} 184 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 185 | -------------------------------------------------------------------------------- /test/lifecycle.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'aegir/chai' 2 | import delay from 'delay' 3 | import sinon from 'sinon' 4 | import { PubSubBaseProtocol } from '../src/index.js' 5 | import { 6 | createPeerId, 7 | PubsubImplementation, 8 | ConnectionPair, 9 | MockRegistrar, 10 | mockIncomingStreamEvent 11 | } from './utils/index.js' 12 | import type { PeerId } from '@libp2p/interface-peer-id' 13 | import type { PublishResult, PubSubRPC, PubSubRPCMessage } from '@libp2p/interface-pubsub' 14 | import type { Registrar } from '@libp2p/interface-registrar' 15 | import type { Uint8ArrayList } from 'uint8arraylist' 16 | 17 | class PubsubProtocol extends PubSubBaseProtocol { 18 | decodeRpc (bytes: Uint8Array): PubSubRPC { 19 | throw new Error('Method not implemented.') 20 | } 21 | 22 | encodeRpc (rpc: PubSubRPC): Uint8Array { 23 | throw new Error('Method not implemented.') 24 | } 25 | 26 | decodeMessage (bytes: Uint8Array | Uint8ArrayList): PubSubRPCMessage { 27 | throw new Error('Method not implemented.') 28 | } 29 | 30 | encodeMessage (rpc: PubSubRPCMessage): Uint8Array { 31 | throw new Error('Method not implemented.') 32 | } 33 | 34 | async publishMessage (): Promise { 35 | throw new Error('Method not implemented.') 36 | } 37 | } 38 | 39 | describe('pubsub base lifecycle', () => { 40 | describe('should start and stop properly', () => { 41 | let pubsub: PubsubProtocol 42 | let sinonMockRegistrar: Registrar 43 | 44 | beforeEach(async () => { 45 | const peerId = await createPeerId() 46 | // @ts-expect-error incomplete implementation 47 | sinonMockRegistrar = { 48 | handle: sinon.stub(), 49 | unhandle: sinon.stub(), 50 | register: sinon.stub().returns(`id-${Math.random()}`), 51 | unregister: sinon.stub() 52 | } 53 | 54 | pubsub = new PubsubProtocol({ 55 | peerId, 56 | registrar: sinonMockRegistrar 57 | }, { 58 | multicodecs: ['/pubsub/1.0.0'] 59 | }) 60 | 61 | expect(pubsub.peers.size).to.be.eql(0) 62 | }) 63 | 64 | afterEach(() => { 65 | sinon.restore() 66 | }) 67 | 68 | it('should be able to start and stop', async () => { 69 | await pubsub.start() 70 | expect(sinonMockRegistrar.handle).to.have.property('calledOnce', true) 71 | expect(sinonMockRegistrar.register).to.have.property('calledOnce', true) 72 | 73 | await pubsub.stop() 74 | expect(sinonMockRegistrar.unhandle).to.have.property('calledOnce', true) 75 | expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', true) 76 | }) 77 | 78 | it('starting should not throw if already started', async () => { 79 | await pubsub.start() 80 | await pubsub.start() 81 | expect(sinonMockRegistrar.handle).to.have.property('calledOnce', true) 82 | expect(sinonMockRegistrar.register).to.have.property('calledOnce', true) 83 | 84 | await pubsub.stop() 85 | expect(sinonMockRegistrar.unhandle).to.have.property('calledOnce', true) 86 | expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', true) 87 | }) 88 | 89 | it('stopping should not throw if not started', async () => { 90 | await pubsub.stop() 91 | expect(sinonMockRegistrar.handle).to.have.property('calledOnce', false) 92 | expect(sinonMockRegistrar.unhandle).to.have.property('calledOnce', false) 93 | expect(sinonMockRegistrar.register).to.have.property('calledOnce', false) 94 | expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', false) 95 | }) 96 | }) 97 | 98 | describe('should be able to register two nodes', () => { 99 | const protocol = '/pubsub/1.0.0' 100 | let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation 101 | let peerIdA: PeerId, peerIdB: PeerId 102 | let registrarA: MockRegistrar 103 | let registrarB: MockRegistrar 104 | 105 | // mount pubsub 106 | beforeEach(async () => { 107 | peerIdA = await createPeerId() 108 | peerIdB = await createPeerId() 109 | 110 | registrarA = new MockRegistrar() 111 | registrarB = new MockRegistrar() 112 | 113 | pubsubA = new PubsubImplementation({ 114 | peerId: peerIdA, 115 | registrar: registrarA 116 | }, { 117 | multicodecs: [protocol] 118 | }) 119 | pubsubB = new PubsubImplementation({ 120 | peerId: peerIdB, 121 | registrar: registrarB 122 | }, { 123 | multicodecs: [protocol] 124 | }) 125 | }) 126 | 127 | // start pubsub 128 | beforeEach(async () => { 129 | await Promise.all([ 130 | pubsubA.start(), 131 | pubsubB.start() 132 | ]) 133 | 134 | expect(registrarA.getHandler(protocol)).to.be.ok() 135 | expect(registrarB.getHandler(protocol)).to.be.ok() 136 | }) 137 | 138 | afterEach(async () => { 139 | sinon.restore() 140 | 141 | await Promise.all([ 142 | pubsubA.stop(), 143 | pubsubB.stop() 144 | ]) 145 | }) 146 | 147 | it('should handle onConnect as expected', async () => { 148 | const topologyA = registrarA.getTopologies(protocol)[0] 149 | const handlerB = registrarB.getHandler(protocol) 150 | 151 | if (topologyA == null || handlerB == null) { 152 | throw new Error(`No handler registered for ${protocol}`) 153 | } 154 | 155 | const [c0, c1] = ConnectionPair() 156 | 157 | // Notify peers of connection 158 | topologyA.onConnect(peerIdB, c0) 159 | handlerB.handler(await mockIncomingStreamEvent(protocol, c1, peerIdA)) 160 | 161 | expect(pubsubA.peers.size).to.be.eql(1) 162 | expect(pubsubB.peers.size).to.be.eql(1) 163 | }) 164 | 165 | it('should use the latest connection if onConnect is called more than once', async () => { 166 | const topologyA = registrarA.getTopologies(protocol)[0] 167 | const handlerB = registrarB.getHandler(protocol) 168 | 169 | if (topologyA == null || handlerB == null) { 170 | throw new Error(`No handler registered for ${protocol}`) 171 | } 172 | 173 | // Notify peers of connection 174 | const [c0, c1] = ConnectionPair() 175 | const [c2] = ConnectionPair() 176 | 177 | sinon.spy(c0, 'newStream') 178 | 179 | topologyA.onConnect(peerIdB, c0) 180 | handlerB.handler(await mockIncomingStreamEvent(protocol, c1, peerIdA)) 181 | expect(c0.newStream).to.have.property('callCount', 1) 182 | 183 | // @ts-expect-error _removePeer is a protected method 184 | sinon.spy(pubsubA, '_removePeer') 185 | 186 | sinon.spy(c2, 'newStream') 187 | 188 | topologyA?.onConnect(peerIdB, c2) 189 | // newStream invocation takes place in a resolved promise 190 | await delay(10) 191 | expect(c2.newStream).to.have.property('callCount', 1) 192 | 193 | // @ts-expect-error _removePeer is a protected method 194 | expect(pubsubA._removePeer).to.have.property('callCount', 0) 195 | 196 | // Verify the first stream was closed 197 | // @ts-expect-error .returnValues is a sinon property 198 | const { stream: firstStream } = await c0.newStream.returnValues[0] 199 | try { 200 | await firstStream.sink(['test']) 201 | } catch (err: any) { 202 | expect(err).to.exist() 203 | return 204 | } 205 | expect.fail('original stream should have ended') 206 | }) 207 | 208 | it('should handle newStream errors in onConnect', async () => { 209 | const topologyA = registrarA.getTopologies(protocol)[0] 210 | const handlerB = registrarB.getHandler(protocol) 211 | 212 | if (topologyA == null || handlerB == null) { 213 | throw new Error(`No handler registered for ${protocol}`) 214 | } 215 | 216 | // Notify peers of connection 217 | const [c0, c1] = ConnectionPair() 218 | const error = new Error('new stream error') 219 | sinon.stub(c0, 'newStream').throws(error) 220 | 221 | topologyA.onConnect(peerIdB, c0) 222 | handlerB.handler(await mockIncomingStreamEvent(protocol, c1, peerIdA)) 223 | 224 | expect(c0.newStream).to.have.property('callCount', 1) 225 | }) 226 | 227 | it('should handle onDisconnect as expected', async () => { 228 | const topologyA = registrarA.getTopologies(protocol)[0] 229 | const topologyB = registrarB.getTopologies(protocol)[0] 230 | const handlerB = registrarB.getHandler(protocol) 231 | 232 | if (topologyA == null || handlerB == null) { 233 | throw new Error(`No handler registered for ${protocol}`) 234 | } 235 | 236 | // Notify peers of connection 237 | const [c0, c1] = ConnectionPair() 238 | 239 | topologyA.onConnect(peerIdB, c0) 240 | handlerB.handler(await mockIncomingStreamEvent(protocol, c1, peerIdA)) 241 | 242 | // Notice peers of disconnect 243 | topologyA?.onDisconnect(peerIdB) 244 | topologyB?.onDisconnect(peerIdA) 245 | 246 | expect(pubsubA.peers.size).to.be.eql(0) 247 | expect(pubsubB.peers.size).to.be.eql(0) 248 | }) 249 | 250 | it('should handle onDisconnect for unknown peers', () => { 251 | const topologyA = registrarA.getTopologies(protocol)[0] 252 | 253 | expect(pubsubA.peers.size).to.be.eql(0) 254 | 255 | // Notice peers of disconnect 256 | topologyA?.onDisconnect(peerIdB) 257 | 258 | expect(pubsubA.peers.size).to.be.eql(0) 259 | }) 260 | }) 261 | }) 262 | -------------------------------------------------------------------------------- /test/pubsub.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint max-nested-callbacks: ["error", 6] */ 2 | import { PeerSet } from '@libp2p/peer-collections' 3 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 4 | import { expect } from 'aegir/chai' 5 | import delay from 'delay' 6 | import pDefer from 'p-defer' 7 | import pWaitFor from 'p-wait-for' 8 | import sinon from 'sinon' 9 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 10 | import { PeerStreams } from '../src/peer-streams.js' 11 | import { noSignMsgId } from '../src/utils.js' 12 | import { 13 | createPeerId, 14 | MockRegistrar, 15 | ConnectionPair, 16 | PubsubImplementation, 17 | mockIncomingStreamEvent 18 | } from './utils/index.js' 19 | import type { PeerId } from '@libp2p/interface-peer-id' 20 | import type { Message, PubSubRPC } from '@libp2p/interface-pubsub' 21 | 22 | const protocol = '/pubsub/1.0.0' 23 | const topic = 'test-topic' 24 | const message = uint8ArrayFromString('hello') 25 | 26 | describe('pubsub base implementation', () => { 27 | describe('publish', () => { 28 | let pubsub: PubsubImplementation 29 | 30 | beforeEach(async () => { 31 | const peerId = await createPeerId() 32 | pubsub = new PubsubImplementation({ 33 | peerId, 34 | registrar: new MockRegistrar() 35 | }, { 36 | multicodecs: [protocol], 37 | emitSelf: true 38 | }) 39 | }) 40 | 41 | afterEach(async () => { await pubsub.stop() }) 42 | 43 | it('calls _publish for router to forward messages', async () => { 44 | sinon.spy(pubsub, 'publishMessage') 45 | 46 | await pubsub.start() 47 | await pubsub.publish(topic, message) 48 | 49 | // event dispatch is async 50 | await pWaitFor(() => { 51 | // @ts-expect-error .callCount is a added by sinon 52 | return pubsub.publishMessage.callCount === 1 53 | }) 54 | 55 | // @ts-expect-error .callCount is a added by sinon 56 | expect(pubsub.publishMessage.callCount).to.eql(1) 57 | }) 58 | 59 | it('should sign messages on publish', async () => { 60 | const publishMessageSpy = sinon.spy(pubsub, 'publishMessage') 61 | 62 | await pubsub.start() 63 | await pubsub.publish(topic, message) 64 | 65 | // event dispatch is async 66 | await pWaitFor(() => { 67 | return publishMessageSpy.callCount === 1 68 | }) 69 | 70 | // Get the first message sent to _publish, and validate it 71 | const signedMessage: Message = publishMessageSpy.getCall(0).lastArg 72 | 73 | await expect(pubsub.validate(pubsub.components.peerId, signedMessage)).to.eventually.be.undefined() 74 | }) 75 | 76 | it('calls publishes messages twice', async () => { 77 | let count = 0 78 | 79 | await pubsub.start() 80 | pubsub.subscribe(topic) 81 | 82 | pubsub.addEventListener('message', evt => { 83 | if (evt.detail.topic === topic) { 84 | count++ 85 | } 86 | }) 87 | await pubsub.publish(topic, message) 88 | await pubsub.publish(topic, message) 89 | 90 | // event dispatch is async 91 | await pWaitFor(() => { 92 | return count === 2 93 | }) 94 | 95 | expect(count).to.eql(2) 96 | }) 97 | }) 98 | 99 | describe('subscribe', () => { 100 | describe('basics', () => { 101 | let pubsub: PubsubImplementation 102 | 103 | beforeEach(async () => { 104 | const peerId = await createPeerId() 105 | pubsub = new PubsubImplementation({ 106 | peerId, 107 | registrar: new MockRegistrar() 108 | }, { 109 | multicodecs: [protocol] 110 | }) 111 | await pubsub.start() 112 | }) 113 | 114 | afterEach(async () => { await pubsub.stop() }) 115 | 116 | it('should add subscription', () => { 117 | pubsub.subscribe(topic) 118 | 119 | expect(pubsub.subscriptions.size).to.eql(1) 120 | expect(pubsub.subscriptions.has(topic)).to.be.true() 121 | }) 122 | }) 123 | 124 | describe('two nodes', () => { 125 | let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation 126 | let peerIdA: PeerId, peerIdB: PeerId 127 | let registrarA: MockRegistrar 128 | let registrarB: MockRegistrar 129 | 130 | beforeEach(async () => { 131 | peerIdA = await createPeerId() 132 | peerIdB = await createPeerId() 133 | 134 | registrarA = new MockRegistrar() 135 | registrarB = new MockRegistrar() 136 | 137 | pubsubA = new PubsubImplementation({ 138 | peerId: peerIdA, 139 | registrar: registrarA 140 | }, { 141 | multicodecs: [protocol] 142 | }) 143 | pubsubB = new PubsubImplementation({ 144 | peerId: peerIdB, 145 | registrar: registrarB 146 | }, { 147 | multicodecs: [protocol] 148 | }) 149 | }) 150 | 151 | // start pubsub and connect nodes 152 | beforeEach(async () => { 153 | await Promise.all([ 154 | pubsubA.start(), 155 | pubsubB.start() 156 | ]) 157 | const topologyA = registrarA.getTopologies(protocol)[0] 158 | const handlerB = registrarB.getHandler(protocol) 159 | 160 | if (topologyA == null || handlerB == null) { 161 | throw new Error(`No handler registered for ${protocol}`) 162 | } 163 | 164 | // Notify peers of connection 165 | const [c0, c1] = ConnectionPair() 166 | 167 | topologyA.onConnect(peerIdB, c0) 168 | handlerB.handler(await mockIncomingStreamEvent(protocol, c1, peerIdA)) 169 | }) 170 | 171 | afterEach(async () => { 172 | await Promise.all([ 173 | pubsubA.stop(), 174 | pubsubB.stop() 175 | ]) 176 | }) 177 | 178 | it('should send subscribe message to connected peers', async () => { 179 | sinon.spy(pubsubA, 'send') 180 | sinon.spy(pubsubB, 'processRpcSubOpt') 181 | 182 | pubsubA.subscribe(topic) 183 | 184 | // Should send subscriptions to a peer 185 | // @ts-expect-error .callCount is a added by sinon 186 | expect(pubsubA.send.callCount).to.eql(1) 187 | 188 | // Other peer should receive subscription message 189 | await pWaitFor(() => { 190 | const subscribers = pubsubB.getSubscribers(topic) 191 | 192 | return subscribers.length === 1 193 | }) 194 | 195 | // @ts-expect-error .callCount is a added by sinon 196 | expect(pubsubB.processRpcSubOpt.callCount).to.eql(1) 197 | }) 198 | }) 199 | }) 200 | 201 | describe('unsubscribe', () => { 202 | describe('basics', () => { 203 | let pubsub: PubsubImplementation 204 | 205 | beforeEach(async () => { 206 | const peerId = await createPeerId() 207 | pubsub = new PubsubImplementation({ 208 | peerId, 209 | registrar: new MockRegistrar() 210 | }, { 211 | multicodecs: [protocol] 212 | }) 213 | await pubsub.start() 214 | }) 215 | 216 | afterEach(async () => { await pubsub.stop() }) 217 | 218 | it('should remove all subscriptions for a topic', () => { 219 | pubsub.subscribe(topic) 220 | pubsub.subscribe(topic) 221 | 222 | expect(pubsub.subscriptions.size).to.eql(1) 223 | 224 | pubsub.unsubscribe(topic) 225 | 226 | expect(pubsub.subscriptions.size).to.eql(0) 227 | }) 228 | }) 229 | 230 | describe('two nodes', () => { 231 | let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation 232 | let peerIdA: PeerId, peerIdB: PeerId 233 | let registrarA: MockRegistrar 234 | let registrarB: MockRegistrar 235 | 236 | beforeEach(async () => { 237 | peerIdA = await createPeerId() 238 | peerIdB = await createPeerId() 239 | 240 | registrarA = new MockRegistrar() 241 | registrarB = new MockRegistrar() 242 | 243 | pubsubA = new PubsubImplementation({ 244 | peerId: peerIdA, 245 | registrar: registrarA 246 | }, { 247 | multicodecs: [protocol] 248 | }) 249 | pubsubB = new PubsubImplementation({ 250 | peerId: peerIdB, 251 | registrar: registrarB 252 | }, { 253 | multicodecs: [protocol] 254 | }) 255 | }) 256 | 257 | // start pubsub and connect nodes 258 | beforeEach(async () => { 259 | await Promise.all([ 260 | pubsubA.start(), 261 | pubsubB.start() 262 | ]) 263 | 264 | const topologyA = registrarA.getTopologies(protocol)[0] 265 | const handlerB = registrarB.getHandler(protocol) 266 | 267 | if (topologyA == null || handlerB == null) { 268 | throw new Error(`No handler registered for ${protocol}`) 269 | } 270 | 271 | // Notify peers of connection 272 | const [c0, c1] = ConnectionPair() 273 | 274 | topologyA.onConnect(peerIdB, c0) 275 | handlerB.handler(await mockIncomingStreamEvent(protocol, c1, peerIdA)) 276 | }) 277 | 278 | afterEach(async () => { 279 | await Promise.all([ 280 | pubsubA.stop(), 281 | pubsubB.stop() 282 | ]) 283 | }) 284 | 285 | it('should send unsubscribe message to connected peers', async () => { 286 | const pubsubASendSpy = sinon.spy(pubsubA, 'send') 287 | const pubsubBProcessRpcSubOptSpy = sinon.spy(pubsubB, 'processRpcSubOpt') 288 | 289 | pubsubA.subscribe(topic) 290 | // Should send subscriptions to a peer 291 | expect(pubsubASendSpy.callCount).to.eql(1) 292 | 293 | // Other peer should receive subscription message 294 | await pWaitFor(() => { 295 | const subscribers = pubsubB.getSubscribers(topic) 296 | 297 | return subscribers.length === 1 298 | }) 299 | 300 | expect(pubsubBProcessRpcSubOptSpy.callCount).to.eql(1) 301 | 302 | // Unsubscribe 303 | pubsubA.unsubscribe(topic) 304 | 305 | // Should send subscriptions to a peer 306 | expect(pubsubASendSpy.callCount).to.eql(2) 307 | 308 | // Other peer should receive subscription message 309 | await pWaitFor(() => { 310 | const subscribers = pubsubB.getSubscribers(topic) 311 | 312 | return subscribers.length === 0 313 | }) 314 | 315 | // @ts-expect-error .callCount is a property added by sinon 316 | expect(pubsubB.processRpcSubOpt.callCount).to.eql(2) 317 | }) 318 | 319 | it('should not send unsubscribe message to connected peers if not subscribed', () => { 320 | const pubsubASendSpy = sinon.spy(pubsubA, 'send') 321 | 322 | // Unsubscribe 323 | pubsubA.unsubscribe(topic) 324 | 325 | // Should send subscriptions to a peer 326 | expect(pubsubASendSpy.callCount).to.eql(0) 327 | }) 328 | }) 329 | }) 330 | 331 | describe('getTopics', () => { 332 | let peerId: PeerId 333 | let pubsub: PubsubImplementation 334 | 335 | beforeEach(async () => { 336 | peerId = await createPeerId() 337 | pubsub = new PubsubImplementation({ 338 | peerId, 339 | registrar: new MockRegistrar() 340 | }, { 341 | multicodecs: [protocol] 342 | }) 343 | await pubsub.start() 344 | }) 345 | 346 | afterEach(async () => { await pubsub.stop() }) 347 | 348 | it('returns the subscribed topics', () => { 349 | let subsTopics = pubsub.getTopics() 350 | expect(subsTopics).to.have.lengthOf(0) 351 | 352 | pubsub.subscribe(topic) 353 | 354 | subsTopics = pubsub.getTopics() 355 | expect(subsTopics).to.have.lengthOf(1) 356 | expect(subsTopics[0]).to.eql(topic) 357 | }) 358 | }) 359 | 360 | describe('getSubscribers', () => { 361 | let peerId: PeerId 362 | let pubsub: PubsubImplementation 363 | 364 | beforeEach(async () => { 365 | peerId = await createPeerId() 366 | pubsub = new PubsubImplementation({ 367 | peerId, 368 | registrar: new MockRegistrar() 369 | }, { 370 | multicodecs: [protocol] 371 | }) 372 | }) 373 | 374 | afterEach(async () => { await pubsub.stop() }) 375 | 376 | it('should fail if pubsub is not started', () => { 377 | const topic = 'test-topic' 378 | 379 | try { 380 | pubsub.getSubscribers(topic) 381 | } catch (err: any) { 382 | expect(err).to.exist() 383 | expect(err.code).to.eql('ERR_NOT_STARTED_YET') 384 | return 385 | } 386 | throw new Error('should fail if pubsub is not started') 387 | }) 388 | 389 | it('should fail if no topic is provided', async () => { 390 | // start pubsub 391 | await pubsub.start() 392 | 393 | try { 394 | // @ts-expect-error invalid params 395 | pubsub.getSubscribers() 396 | } catch (err: any) { 397 | expect(err).to.exist() 398 | expect(err.code).to.eql('ERR_NOT_VALID_TOPIC') 399 | return 400 | } 401 | throw new Error('should fail if no topic is provided') 402 | }) 403 | 404 | it('should get peer subscribed to one topic', async () => { 405 | const topic = 'test-topic' 406 | 407 | // start pubsub 408 | await pubsub.start() 409 | 410 | let peersSubscribed = pubsub.getSubscribers(topic) 411 | expect(peersSubscribed).to.be.empty() 412 | 413 | // Set mock peer subscribed 414 | const peer = new PeerStreams({ id: peerId, protocol: 'a-protocol' }) 415 | const id = peer.id 416 | 417 | const set = new PeerSet() 418 | set.add(id) 419 | 420 | pubsub.topics.set(topic, set) 421 | pubsub.peers.set(peer.id, peer) 422 | 423 | peersSubscribed = pubsub.getSubscribers(topic) 424 | 425 | expect(peersSubscribed).to.not.be.empty() 426 | expect(id.equals(peersSubscribed[0])).to.be.true() 427 | }) 428 | }) 429 | 430 | describe('verification', () => { 431 | let peerId: PeerId 432 | let pubsub: PubsubImplementation 433 | const data = uint8ArrayFromString('bar') 434 | 435 | beforeEach(async () => { 436 | peerId = await createPeerId() 437 | pubsub = new PubsubImplementation({ 438 | peerId, 439 | registrar: new MockRegistrar() 440 | }, { 441 | multicodecs: [protocol] 442 | }) 443 | await pubsub.start() 444 | }) 445 | 446 | afterEach(async () => { await pubsub.stop() }) 447 | 448 | it('should drop unsigned messages', async () => { 449 | const publishSpy = sinon.spy(pubsub, 'publishMessage') 450 | sinon.spy(pubsub, 'validate') 451 | 452 | const peerStream = new PeerStreams({ 453 | id: await createEd25519PeerId(), 454 | protocol: 'test' 455 | }) 456 | const rpc: PubSubRPC = { 457 | subscriptions: [], 458 | messages: [{ 459 | from: peerStream.id.toBytes(), 460 | data, 461 | sequenceNumber: await noSignMsgId(data), 462 | topic 463 | }] 464 | } 465 | 466 | pubsub.subscribe(topic) 467 | 468 | await pubsub.processRpc(peerStream.id, peerStream, rpc) 469 | 470 | // message should not be delivered 471 | await delay(1000) 472 | 473 | expect(publishSpy).to.have.property('called', false) 474 | }) 475 | 476 | it('should not drop unsigned messages if strict signing is disabled', async () => { 477 | pubsub.globalSignaturePolicy = 'StrictNoSign' 478 | 479 | const publishSpy = sinon.spy(pubsub, 'publishMessage') 480 | sinon.spy(pubsub, 'validate') 481 | 482 | const peerStream = new PeerStreams({ 483 | id: await createEd25519PeerId(), 484 | protocol: 'test' 485 | }) 486 | 487 | const rpc: PubSubRPC = { 488 | subscriptions: [], 489 | messages: [{ 490 | from: peerStream.id.toBytes(), 491 | data, 492 | topic 493 | }] 494 | } 495 | 496 | pubsub.subscribe(topic) 497 | 498 | const deferred = pDefer() 499 | 500 | pubsub.addEventListener('message', (evt) => { 501 | if (evt.detail.topic === topic) { 502 | deferred.resolve() 503 | } 504 | }) 505 | 506 | await pubsub.processRpc(peerStream.id, peerStream, rpc) 507 | 508 | // await message delivery 509 | await deferred.promise 510 | 511 | expect(pubsub.validate).to.have.property('callCount', 1) 512 | expect(publishSpy).to.have.property('callCount', 1) 513 | }) 514 | }) 515 | }) 516 | -------------------------------------------------------------------------------- /test/message/rpc.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | /* eslint-disable complexity */ 3 | /* eslint-disable @typescript-eslint/no-namespace */ 4 | /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ 5 | /* eslint-disable @typescript-eslint/no-empty-interface */ 6 | 7 | import { encodeMessage, decodeMessage, message } from 'protons-runtime' 8 | import type { Codec } from 'protons-runtime' 9 | import type { Uint8ArrayList } from 'uint8arraylist' 10 | 11 | export interface RPC { 12 | subscriptions: RPC.SubOpts[] 13 | messages: RPC.Message[] 14 | control?: ControlMessage 15 | } 16 | 17 | export namespace RPC { 18 | export interface SubOpts { 19 | subscribe?: boolean 20 | topic?: string 21 | } 22 | 23 | export namespace SubOpts { 24 | let _codec: Codec 25 | 26 | export const codec = (): Codec => { 27 | if (_codec == null) { 28 | _codec = message((obj, w, opts = {}) => { 29 | if (opts.lengthDelimited !== false) { 30 | w.fork() 31 | } 32 | 33 | if (obj.subscribe != null) { 34 | w.uint32(8) 35 | w.bool(obj.subscribe) 36 | } 37 | 38 | if (obj.topic != null) { 39 | w.uint32(18) 40 | w.string(obj.topic) 41 | } 42 | 43 | if (opts.lengthDelimited !== false) { 44 | w.ldelim() 45 | } 46 | }, (reader, length) => { 47 | const obj: any = {} 48 | 49 | const end = length == null ? reader.len : reader.pos + length 50 | 51 | while (reader.pos < end) { 52 | const tag = reader.uint32() 53 | 54 | switch (tag >>> 3) { 55 | case 1: 56 | obj.subscribe = reader.bool() 57 | break 58 | case 2: 59 | obj.topic = reader.string() 60 | break 61 | default: 62 | reader.skipType(tag & 7) 63 | break 64 | } 65 | } 66 | 67 | return obj 68 | }) 69 | } 70 | 71 | return _codec 72 | } 73 | 74 | export const encode = (obj: Partial): Uint8Array => { 75 | return encodeMessage(obj, SubOpts.codec()) 76 | } 77 | 78 | export const decode = (buf: Uint8Array | Uint8ArrayList): SubOpts => { 79 | return decodeMessage(buf, SubOpts.codec()) 80 | } 81 | } 82 | 83 | export interface Message { 84 | from?: Uint8Array 85 | data?: Uint8Array 86 | seqno?: Uint8Array 87 | topic?: string 88 | signature?: Uint8Array 89 | key?: Uint8Array 90 | } 91 | 92 | export namespace Message { 93 | let _codec: Codec 94 | 95 | export const codec = (): Codec => { 96 | if (_codec == null) { 97 | _codec = message((obj, w, opts = {}) => { 98 | if (opts.lengthDelimited !== false) { 99 | w.fork() 100 | } 101 | 102 | if (obj.from != null) { 103 | w.uint32(10) 104 | w.bytes(obj.from) 105 | } 106 | 107 | if (obj.data != null) { 108 | w.uint32(18) 109 | w.bytes(obj.data) 110 | } 111 | 112 | if (obj.seqno != null) { 113 | w.uint32(26) 114 | w.bytes(obj.seqno) 115 | } 116 | 117 | if (obj.topic != null) { 118 | w.uint32(34) 119 | w.string(obj.topic) 120 | } 121 | 122 | if (obj.signature != null) { 123 | w.uint32(42) 124 | w.bytes(obj.signature) 125 | } 126 | 127 | if (obj.key != null) { 128 | w.uint32(50) 129 | w.bytes(obj.key) 130 | } 131 | 132 | if (opts.lengthDelimited !== false) { 133 | w.ldelim() 134 | } 135 | }, (reader, length) => { 136 | const obj: any = {} 137 | 138 | const end = length == null ? reader.len : reader.pos + length 139 | 140 | while (reader.pos < end) { 141 | const tag = reader.uint32() 142 | 143 | switch (tag >>> 3) { 144 | case 1: 145 | obj.from = reader.bytes() 146 | break 147 | case 2: 148 | obj.data = reader.bytes() 149 | break 150 | case 3: 151 | obj.seqno = reader.bytes() 152 | break 153 | case 4: 154 | obj.topic = reader.string() 155 | break 156 | case 5: 157 | obj.signature = reader.bytes() 158 | break 159 | case 6: 160 | obj.key = reader.bytes() 161 | break 162 | default: 163 | reader.skipType(tag & 7) 164 | break 165 | } 166 | } 167 | 168 | return obj 169 | }) 170 | } 171 | 172 | return _codec 173 | } 174 | 175 | export const encode = (obj: Partial): Uint8Array => { 176 | return encodeMessage(obj, Message.codec()) 177 | } 178 | 179 | export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { 180 | return decodeMessage(buf, Message.codec()) 181 | } 182 | } 183 | 184 | let _codec: Codec 185 | 186 | export const codec = (): Codec => { 187 | if (_codec == null) { 188 | _codec = message((obj, w, opts = {}) => { 189 | if (opts.lengthDelimited !== false) { 190 | w.fork() 191 | } 192 | 193 | if (obj.subscriptions != null) { 194 | for (const value of obj.subscriptions) { 195 | w.uint32(10) 196 | RPC.SubOpts.codec().encode(value, w) 197 | } 198 | } 199 | 200 | if (obj.messages != null) { 201 | for (const value of obj.messages) { 202 | w.uint32(18) 203 | RPC.Message.codec().encode(value, w) 204 | } 205 | } 206 | 207 | if (obj.control != null) { 208 | w.uint32(26) 209 | ControlMessage.codec().encode(obj.control, w) 210 | } 211 | 212 | if (opts.lengthDelimited !== false) { 213 | w.ldelim() 214 | } 215 | }, (reader, length) => { 216 | const obj: any = { 217 | subscriptions: [], 218 | messages: [] 219 | } 220 | 221 | const end = length == null ? reader.len : reader.pos + length 222 | 223 | while (reader.pos < end) { 224 | const tag = reader.uint32() 225 | 226 | switch (tag >>> 3) { 227 | case 1: 228 | obj.subscriptions.push(RPC.SubOpts.codec().decode(reader, reader.uint32())) 229 | break 230 | case 2: 231 | obj.messages.push(RPC.Message.codec().decode(reader, reader.uint32())) 232 | break 233 | case 3: 234 | obj.control = ControlMessage.codec().decode(reader, reader.uint32()) 235 | break 236 | default: 237 | reader.skipType(tag & 7) 238 | break 239 | } 240 | } 241 | 242 | return obj 243 | }) 244 | } 245 | 246 | return _codec 247 | } 248 | 249 | export const encode = (obj: Partial): Uint8Array => { 250 | return encodeMessage(obj, RPC.codec()) 251 | } 252 | 253 | export const decode = (buf: Uint8Array | Uint8ArrayList): RPC => { 254 | return decodeMessage(buf, RPC.codec()) 255 | } 256 | } 257 | 258 | export interface ControlMessage { 259 | ihave: ControlIHave[] 260 | iwant: ControlIWant[] 261 | graft: ControlGraft[] 262 | prune: ControlPrune[] 263 | } 264 | 265 | export namespace ControlMessage { 266 | let _codec: Codec 267 | 268 | export const codec = (): Codec => { 269 | if (_codec == null) { 270 | _codec = message((obj, w, opts = {}) => { 271 | if (opts.lengthDelimited !== false) { 272 | w.fork() 273 | } 274 | 275 | if (obj.ihave != null) { 276 | for (const value of obj.ihave) { 277 | w.uint32(10) 278 | ControlIHave.codec().encode(value, w) 279 | } 280 | } 281 | 282 | if (obj.iwant != null) { 283 | for (const value of obj.iwant) { 284 | w.uint32(18) 285 | ControlIWant.codec().encode(value, w) 286 | } 287 | } 288 | 289 | if (obj.graft != null) { 290 | for (const value of obj.graft) { 291 | w.uint32(26) 292 | ControlGraft.codec().encode(value, w) 293 | } 294 | } 295 | 296 | if (obj.prune != null) { 297 | for (const value of obj.prune) { 298 | w.uint32(34) 299 | ControlPrune.codec().encode(value, w) 300 | } 301 | } 302 | 303 | if (opts.lengthDelimited !== false) { 304 | w.ldelim() 305 | } 306 | }, (reader, length) => { 307 | const obj: any = { 308 | ihave: [], 309 | iwant: [], 310 | graft: [], 311 | prune: [] 312 | } 313 | 314 | const end = length == null ? reader.len : reader.pos + length 315 | 316 | while (reader.pos < end) { 317 | const tag = reader.uint32() 318 | 319 | switch (tag >>> 3) { 320 | case 1: 321 | obj.ihave.push(ControlIHave.codec().decode(reader, reader.uint32())) 322 | break 323 | case 2: 324 | obj.iwant.push(ControlIWant.codec().decode(reader, reader.uint32())) 325 | break 326 | case 3: 327 | obj.graft.push(ControlGraft.codec().decode(reader, reader.uint32())) 328 | break 329 | case 4: 330 | obj.prune.push(ControlPrune.codec().decode(reader, reader.uint32())) 331 | break 332 | default: 333 | reader.skipType(tag & 7) 334 | break 335 | } 336 | } 337 | 338 | return obj 339 | }) 340 | } 341 | 342 | return _codec 343 | } 344 | 345 | export const encode = (obj: Partial): Uint8Array => { 346 | return encodeMessage(obj, ControlMessage.codec()) 347 | } 348 | 349 | export const decode = (buf: Uint8Array | Uint8ArrayList): ControlMessage => { 350 | return decodeMessage(buf, ControlMessage.codec()) 351 | } 352 | } 353 | 354 | export interface ControlIHave { 355 | topic?: string 356 | messageIDs: Uint8Array[] 357 | } 358 | 359 | export namespace ControlIHave { 360 | let _codec: Codec 361 | 362 | export const codec = (): Codec => { 363 | if (_codec == null) { 364 | _codec = message((obj, w, opts = {}) => { 365 | if (opts.lengthDelimited !== false) { 366 | w.fork() 367 | } 368 | 369 | if (obj.topic != null) { 370 | w.uint32(10) 371 | w.string(obj.topic) 372 | } 373 | 374 | if (obj.messageIDs != null) { 375 | for (const value of obj.messageIDs) { 376 | w.uint32(18) 377 | w.bytes(value) 378 | } 379 | } 380 | 381 | if (opts.lengthDelimited !== false) { 382 | w.ldelim() 383 | } 384 | }, (reader, length) => { 385 | const obj: any = { 386 | messageIDs: [] 387 | } 388 | 389 | const end = length == null ? reader.len : reader.pos + length 390 | 391 | while (reader.pos < end) { 392 | const tag = reader.uint32() 393 | 394 | switch (tag >>> 3) { 395 | case 1: 396 | obj.topic = reader.string() 397 | break 398 | case 2: 399 | obj.messageIDs.push(reader.bytes()) 400 | break 401 | default: 402 | reader.skipType(tag & 7) 403 | break 404 | } 405 | } 406 | 407 | return obj 408 | }) 409 | } 410 | 411 | return _codec 412 | } 413 | 414 | export const encode = (obj: Partial): Uint8Array => { 415 | return encodeMessage(obj, ControlIHave.codec()) 416 | } 417 | 418 | export const decode = (buf: Uint8Array | Uint8ArrayList): ControlIHave => { 419 | return decodeMessage(buf, ControlIHave.codec()) 420 | } 421 | } 422 | 423 | export interface ControlIWant { 424 | messageIDs: Uint8Array[] 425 | } 426 | 427 | export namespace ControlIWant { 428 | let _codec: Codec 429 | 430 | export const codec = (): Codec => { 431 | if (_codec == null) { 432 | _codec = message((obj, w, opts = {}) => { 433 | if (opts.lengthDelimited !== false) { 434 | w.fork() 435 | } 436 | 437 | if (obj.messageIDs != null) { 438 | for (const value of obj.messageIDs) { 439 | w.uint32(10) 440 | w.bytes(value) 441 | } 442 | } 443 | 444 | if (opts.lengthDelimited !== false) { 445 | w.ldelim() 446 | } 447 | }, (reader, length) => { 448 | const obj: any = { 449 | messageIDs: [] 450 | } 451 | 452 | const end = length == null ? reader.len : reader.pos + length 453 | 454 | while (reader.pos < end) { 455 | const tag = reader.uint32() 456 | 457 | switch (tag >>> 3) { 458 | case 1: 459 | obj.messageIDs.push(reader.bytes()) 460 | break 461 | default: 462 | reader.skipType(tag & 7) 463 | break 464 | } 465 | } 466 | 467 | return obj 468 | }) 469 | } 470 | 471 | return _codec 472 | } 473 | 474 | export const encode = (obj: Partial): Uint8Array => { 475 | return encodeMessage(obj, ControlIWant.codec()) 476 | } 477 | 478 | export const decode = (buf: Uint8Array | Uint8ArrayList): ControlIWant => { 479 | return decodeMessage(buf, ControlIWant.codec()) 480 | } 481 | } 482 | 483 | export interface ControlGraft { 484 | topic?: string 485 | } 486 | 487 | export namespace ControlGraft { 488 | let _codec: Codec 489 | 490 | export const codec = (): Codec => { 491 | if (_codec == null) { 492 | _codec = message((obj, w, opts = {}) => { 493 | if (opts.lengthDelimited !== false) { 494 | w.fork() 495 | } 496 | 497 | if (obj.topic != null) { 498 | w.uint32(10) 499 | w.string(obj.topic) 500 | } 501 | 502 | if (opts.lengthDelimited !== false) { 503 | w.ldelim() 504 | } 505 | }, (reader, length) => { 506 | const obj: any = {} 507 | 508 | const end = length == null ? reader.len : reader.pos + length 509 | 510 | while (reader.pos < end) { 511 | const tag = reader.uint32() 512 | 513 | switch (tag >>> 3) { 514 | case 1: 515 | obj.topic = reader.string() 516 | break 517 | default: 518 | reader.skipType(tag & 7) 519 | break 520 | } 521 | } 522 | 523 | return obj 524 | }) 525 | } 526 | 527 | return _codec 528 | } 529 | 530 | export const encode = (obj: Partial): Uint8Array => { 531 | return encodeMessage(obj, ControlGraft.codec()) 532 | } 533 | 534 | export const decode = (buf: Uint8Array | Uint8ArrayList): ControlGraft => { 535 | return decodeMessage(buf, ControlGraft.codec()) 536 | } 537 | } 538 | 539 | export interface ControlPrune { 540 | topic?: string 541 | peers: PeerInfo[] 542 | backoff?: bigint 543 | } 544 | 545 | export namespace ControlPrune { 546 | let _codec: Codec 547 | 548 | export const codec = (): Codec => { 549 | if (_codec == null) { 550 | _codec = message((obj, w, opts = {}) => { 551 | if (opts.lengthDelimited !== false) { 552 | w.fork() 553 | } 554 | 555 | if (obj.topic != null) { 556 | w.uint32(10) 557 | w.string(obj.topic) 558 | } 559 | 560 | if (obj.peers != null) { 561 | for (const value of obj.peers) { 562 | w.uint32(18) 563 | PeerInfo.codec().encode(value, w) 564 | } 565 | } 566 | 567 | if (obj.backoff != null) { 568 | w.uint32(24) 569 | w.uint64(obj.backoff) 570 | } 571 | 572 | if (opts.lengthDelimited !== false) { 573 | w.ldelim() 574 | } 575 | }, (reader, length) => { 576 | const obj: any = { 577 | peers: [] 578 | } 579 | 580 | const end = length == null ? reader.len : reader.pos + length 581 | 582 | while (reader.pos < end) { 583 | const tag = reader.uint32() 584 | 585 | switch (tag >>> 3) { 586 | case 1: 587 | obj.topic = reader.string() 588 | break 589 | case 2: 590 | obj.peers.push(PeerInfo.codec().decode(reader, reader.uint32())) 591 | break 592 | case 3: 593 | obj.backoff = reader.uint64() 594 | break 595 | default: 596 | reader.skipType(tag & 7) 597 | break 598 | } 599 | } 600 | 601 | return obj 602 | }) 603 | } 604 | 605 | return _codec 606 | } 607 | 608 | export const encode = (obj: Partial): Uint8Array => { 609 | return encodeMessage(obj, ControlPrune.codec()) 610 | } 611 | 612 | export const decode = (buf: Uint8Array | Uint8ArrayList): ControlPrune => { 613 | return decodeMessage(buf, ControlPrune.codec()) 614 | } 615 | } 616 | 617 | export interface PeerInfo { 618 | peerID?: Uint8Array 619 | signedPeerRecord?: Uint8Array 620 | } 621 | 622 | export namespace PeerInfo { 623 | let _codec: Codec 624 | 625 | export const codec = (): Codec => { 626 | if (_codec == null) { 627 | _codec = message((obj, w, opts = {}) => { 628 | if (opts.lengthDelimited !== false) { 629 | w.fork() 630 | } 631 | 632 | if (obj.peerID != null) { 633 | w.uint32(10) 634 | w.bytes(obj.peerID) 635 | } 636 | 637 | if (obj.signedPeerRecord != null) { 638 | w.uint32(18) 639 | w.bytes(obj.signedPeerRecord) 640 | } 641 | 642 | if (opts.lengthDelimited !== false) { 643 | w.ldelim() 644 | } 645 | }, (reader, length) => { 646 | const obj: any = {} 647 | 648 | const end = length == null ? reader.len : reader.pos + length 649 | 650 | while (reader.pos < end) { 651 | const tag = reader.uint32() 652 | 653 | switch (tag >>> 3) { 654 | case 1: 655 | obj.peerID = reader.bytes() 656 | break 657 | case 2: 658 | obj.signedPeerRecord = reader.bytes() 659 | break 660 | default: 661 | reader.skipType(tag & 7) 662 | break 663 | } 664 | } 665 | 666 | return obj 667 | }) 668 | } 669 | 670 | return _codec 671 | } 672 | 673 | export const encode = (obj: Partial): Uint8Array => { 674 | return encodeMessage(obj, PeerInfo.codec()) 675 | } 676 | 677 | export const decode = (buf: Uint8Array | Uint8ArrayList): PeerInfo => { 678 | return decodeMessage(buf, PeerInfo.codec()) 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { type PubSub, type Message, type StrictNoSign, type StrictSign, type PubSubInit, type PubSubEvents, type PeerStreams, type PubSubRPCMessage, type PubSubRPC, type PubSubRPCSubscription, type SubscriptionChangeData, type PublishResult, type TopicValidatorFn, TopicValidatorResult } from '@libp2p/interface-pubsub' 2 | import { CodeError } from '@libp2p/interfaces/errors' 3 | import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' 4 | import { logger } from '@libp2p/logger' 5 | import { PeerMap, PeerSet } from '@libp2p/peer-collections' 6 | import { createTopology } from '@libp2p/topology' 7 | import { pipe } from 'it-pipe' 8 | import Queue from 'p-queue' 9 | import { codes } from './errors.js' 10 | import { PeerStreams as PeerStreamsImpl } from './peer-streams.js' 11 | import { 12 | signMessage, 13 | verifySignature 14 | } from './sign.js' 15 | import { toMessage, ensureArray, noSignMsgId, msgId, toRpcMessage, randomSeqno } from './utils.js' 16 | import type { Connection } from '@libp2p/interface-connection' 17 | import type { PeerId } from '@libp2p/interface-peer-id' 18 | import type { IncomingStreamData, Registrar } from '@libp2p/interface-registrar' 19 | import type { Uint8ArrayList } from 'uint8arraylist' 20 | 21 | const log = logger('libp2p:pubsub') 22 | 23 | export interface PubSubComponents { 24 | peerId: PeerId 25 | registrar: Registrar 26 | } 27 | 28 | /** 29 | * PubSubBaseProtocol handles the peers and connections logic for pubsub routers 30 | * and specifies the API that pubsub routers should have. 31 | */ 32 | export abstract class PubSubBaseProtocol = PubSubEvents> extends EventEmitter implements PubSub { 33 | public started: boolean 34 | /** 35 | * Map of topics to which peers are subscribed to 36 | */ 37 | public topics: Map 38 | /** 39 | * List of our subscriptions 40 | */ 41 | public subscriptions: Set 42 | /** 43 | * Map of peer streams 44 | */ 45 | public peers: PeerMap 46 | /** 47 | * The signature policy to follow by default 48 | */ 49 | public globalSignaturePolicy: typeof StrictNoSign | typeof StrictSign 50 | /** 51 | * If router can relay received messages, even if not subscribed 52 | */ 53 | public canRelayMessage: boolean 54 | /** 55 | * if publish should emit to self, if subscribed 56 | */ 57 | public emitSelf: boolean 58 | /** 59 | * Topic validator map 60 | * 61 | * Keyed by topic 62 | * Topic validators are functions with the following input: 63 | */ 64 | public topicValidators: Map 65 | public queue: Queue 66 | public multicodecs: string[] 67 | public components: PubSubComponents 68 | 69 | private _registrarTopologyIds: string[] | undefined 70 | protected enabled: boolean 71 | private readonly maxInboundStreams: number 72 | private readonly maxOutboundStreams: number 73 | 74 | constructor (components: PubSubComponents, props: PubSubInit) { 75 | super() 76 | 77 | const { 78 | multicodecs = [], 79 | globalSignaturePolicy = 'StrictSign', 80 | canRelayMessage = false, 81 | emitSelf = false, 82 | messageProcessingConcurrency = 10, 83 | maxInboundStreams = 1, 84 | maxOutboundStreams = 1 85 | } = props 86 | 87 | this.components = components 88 | this.multicodecs = ensureArray(multicodecs) 89 | this.enabled = props.enabled !== false 90 | this.started = false 91 | this.topics = new Map() 92 | this.subscriptions = new Set() 93 | this.peers = new PeerMap() 94 | this.globalSignaturePolicy = globalSignaturePolicy === 'StrictNoSign' ? 'StrictNoSign' : 'StrictSign' 95 | this.canRelayMessage = canRelayMessage 96 | this.emitSelf = emitSelf 97 | this.topicValidators = new Map() 98 | this.queue = new Queue({ concurrency: messageProcessingConcurrency }) 99 | this.maxInboundStreams = maxInboundStreams 100 | this.maxOutboundStreams = maxOutboundStreams 101 | 102 | this._onIncomingStream = this._onIncomingStream.bind(this) 103 | this._onPeerConnected = this._onPeerConnected.bind(this) 104 | this._onPeerDisconnected = this._onPeerDisconnected.bind(this) 105 | } 106 | 107 | // LIFECYCLE METHODS 108 | 109 | /** 110 | * Register the pubsub protocol onto the libp2p node. 111 | */ 112 | async start (): Promise { 113 | if (this.started || !this.enabled) { 114 | return 115 | } 116 | 117 | log('starting') 118 | 119 | const registrar = this.components.registrar 120 | // Incoming streams 121 | // Called after a peer dials us 122 | await Promise.all(this.multicodecs.map(async multicodec => { 123 | await registrar.handle(multicodec, this._onIncomingStream, { 124 | maxInboundStreams: this.maxInboundStreams, 125 | maxOutboundStreams: this.maxOutboundStreams 126 | }) 127 | })) 128 | 129 | // register protocol with topology 130 | // Topology callbacks called on connection manager changes 131 | const topology = createTopology({ 132 | onConnect: this._onPeerConnected, 133 | onDisconnect: this._onPeerDisconnected 134 | }) 135 | this._registrarTopologyIds = await Promise.all(this.multicodecs.map(async multicodec => registrar.register(multicodec, topology))) 136 | 137 | log('started') 138 | this.started = true 139 | } 140 | 141 | /** 142 | * Unregister the pubsub protocol and the streams with other peers will be closed. 143 | */ 144 | async stop (): Promise { 145 | if (!this.started || !this.enabled) { 146 | return 147 | } 148 | 149 | const registrar = this.components.registrar 150 | 151 | // unregister protocol and handlers 152 | if (this._registrarTopologyIds != null) { 153 | this._registrarTopologyIds?.forEach(id => { 154 | registrar.unregister(id) 155 | }) 156 | } 157 | 158 | await Promise.all(this.multicodecs.map(async multicodec => { 159 | await registrar.unhandle(multicodec) 160 | })) 161 | 162 | log('stopping') 163 | for (const peerStreams of this.peers.values()) { 164 | peerStreams.close() 165 | } 166 | 167 | this.peers.clear() 168 | this.subscriptions = new Set() 169 | this.started = false 170 | log('stopped') 171 | } 172 | 173 | isStarted (): boolean { 174 | return this.started 175 | } 176 | 177 | /** 178 | * On an inbound stream opened 179 | */ 180 | protected _onIncomingStream (data: IncomingStreamData): void { 181 | const { stream, connection } = data 182 | const peerId = connection.remotePeer 183 | 184 | if (stream.stat.protocol == null) { 185 | stream.abort(new Error('Stream was not multiplexed')) 186 | return 187 | } 188 | 189 | const peer = this.addPeer(peerId, stream.stat.protocol) 190 | const inboundStream = peer.attachInboundStream(stream) 191 | 192 | this.processMessages(peerId, inboundStream, peer) 193 | .catch(err => { log(err) }) 194 | } 195 | 196 | /** 197 | * Registrar notifies an established connection with pubsub protocol 198 | */ 199 | protected _onPeerConnected (peerId: PeerId, conn: Connection): void { 200 | log('connected %p', peerId) 201 | 202 | void Promise.resolve().then(async () => { 203 | try { 204 | const stream = await conn.newStream(this.multicodecs) 205 | 206 | if (stream.stat.protocol == null) { 207 | stream.abort(new Error('Stream was not multiplexed')) 208 | return 209 | } 210 | 211 | const peer = this.addPeer(peerId, stream.stat.protocol) 212 | await peer.attachOutboundStream(stream) 213 | } catch (err: any) { 214 | log.error(err) 215 | } 216 | 217 | // Immediately send my own subscriptions to the newly established conn 218 | this.send(peerId, { subscriptions: Array.from(this.subscriptions).map(sub => sub.toString()), subscribe: true }) 219 | }) 220 | .catch(err => { 221 | log.error(err) 222 | }) 223 | } 224 | 225 | /** 226 | * Registrar notifies a closing connection with pubsub protocol 227 | */ 228 | protected _onPeerDisconnected (peerId: PeerId, conn?: Connection): void { 229 | const idB58Str = peerId.toString() 230 | 231 | log('connection ended', idB58Str) 232 | this._removePeer(peerId) 233 | } 234 | 235 | /** 236 | * Notifies the router that a peer has been connected 237 | */ 238 | addPeer (peerId: PeerId, protocol: string): PeerStreams { 239 | const existing = this.peers.get(peerId) 240 | 241 | // If peer streams already exists, do nothing 242 | if (existing != null) { 243 | return existing 244 | } 245 | 246 | // else create a new peer streams 247 | log('new peer %p', peerId) 248 | 249 | const peerStreams: PeerStreams = new PeerStreamsImpl({ 250 | id: peerId, 251 | protocol 252 | }) 253 | 254 | this.peers.set(peerId, peerStreams) 255 | peerStreams.addEventListener('close', () => this._removePeer(peerId), { 256 | once: true 257 | }) 258 | 259 | return peerStreams 260 | } 261 | 262 | /** 263 | * Notifies the router that a peer has been disconnected 264 | */ 265 | protected _removePeer (peerId: PeerId): PeerStreams | undefined { 266 | const peerStreams = this.peers.get(peerId) 267 | if (peerStreams == null) { 268 | return 269 | } 270 | 271 | // close peer streams 272 | peerStreams.close() 273 | 274 | // delete peer streams 275 | log('delete peer %p', peerId) 276 | this.peers.delete(peerId) 277 | 278 | // remove peer from topics map 279 | for (const peers of this.topics.values()) { 280 | peers.delete(peerId) 281 | } 282 | 283 | return peerStreams 284 | } 285 | 286 | // MESSAGE METHODS 287 | 288 | /** 289 | * Responsible for processing each RPC message received by other peers. 290 | */ 291 | async processMessages (peerId: PeerId, stream: AsyncIterable, peerStreams: PeerStreams): Promise { 292 | try { 293 | await pipe( 294 | stream, 295 | async (source) => { 296 | for await (const data of source) { 297 | const rpcMsg = this.decodeRpc(data) 298 | const messages: PubSubRPCMessage[] = [] 299 | 300 | for (const msg of (rpcMsg.messages ?? [])) { 301 | if (msg.from == null || msg.data == null || msg.topic == null) { 302 | log('message from %p was missing from, data or topic fields, dropping', peerId) 303 | continue 304 | } 305 | 306 | messages.push({ 307 | from: msg.from, 308 | data: msg.data, 309 | topic: msg.topic, 310 | sequenceNumber: msg.sequenceNumber ?? undefined, 311 | signature: msg.signature ?? undefined, 312 | key: msg.key ?? undefined 313 | }) 314 | } 315 | 316 | // Since processRpc may be overridden entirely in unsafe ways, 317 | // the simplest/safest option here is to wrap in a function and capture all errors 318 | // to prevent a top-level unhandled exception 319 | // This processing of rpc messages should happen without awaiting full validation/execution of prior messages 320 | this.processRpc(peerId, peerStreams, { 321 | subscriptions: (rpcMsg.subscriptions ?? []).map(sub => ({ 322 | subscribe: Boolean(sub.subscribe), 323 | topic: sub.topic ?? '' 324 | })), 325 | messages 326 | }) 327 | .catch(err => { log(err) }) 328 | } 329 | } 330 | ) 331 | } catch (err: any) { 332 | this._onPeerDisconnected(peerStreams.id, err) 333 | } 334 | } 335 | 336 | /** 337 | * Handles an rpc request from a peer 338 | */ 339 | async processRpc (from: PeerId, peerStreams: PeerStreams, rpc: PubSubRPC): Promise { 340 | if (!this.acceptFrom(from)) { 341 | log('received message from unacceptable peer %p', from) 342 | return false 343 | } 344 | 345 | log('rpc from %p', from) 346 | 347 | const { subscriptions, messages } = rpc 348 | 349 | if (subscriptions != null && subscriptions.length > 0) { 350 | log('subscription update from %p', from) 351 | 352 | // update peer subscriptions 353 | subscriptions.forEach((subOpt) => { 354 | this.processRpcSubOpt(from, subOpt) 355 | }) 356 | 357 | super.dispatchEvent(new CustomEvent('subscription-change', { 358 | detail: { 359 | peerId: peerStreams.id, 360 | subscriptions: subscriptions.map(({ topic, subscribe }) => ({ 361 | topic: `${topic ?? ''}`, 362 | subscribe: Boolean(subscribe) 363 | })) 364 | } 365 | })) 366 | } 367 | 368 | if (messages != null && messages.length > 0) { 369 | log('messages from %p', from) 370 | 371 | this.queue.addAll(messages.map(message => async () => { 372 | if (message.topic == null || (!this.subscriptions.has(message.topic) && !this.canRelayMessage)) { 373 | log('received message we didn\'t subscribe to. Dropping.') 374 | return false 375 | } 376 | 377 | try { 378 | const msg = await toMessage(message) 379 | 380 | await this.processMessage(from, msg) 381 | } catch (err: any) { 382 | log.error(err) 383 | } 384 | })) 385 | .catch(err => { log(err) }) 386 | } 387 | 388 | return true 389 | } 390 | 391 | /** 392 | * Handles a subscription change from a peer 393 | */ 394 | processRpcSubOpt (id: PeerId, subOpt: PubSubRPCSubscription): void { 395 | const t = subOpt.topic 396 | 397 | if (t == null) { 398 | return 399 | } 400 | 401 | let topicSet = this.topics.get(t) 402 | if (topicSet == null) { 403 | topicSet = new PeerSet() 404 | this.topics.set(t, topicSet) 405 | } 406 | 407 | if (subOpt.subscribe === true) { 408 | // subscribe peer to new topic 409 | topicSet.add(id) 410 | } else { 411 | // unsubscribe from existing topic 412 | topicSet.delete(id) 413 | } 414 | } 415 | 416 | /** 417 | * Handles a message from a peer 418 | */ 419 | async processMessage (from: PeerId, msg: Message): Promise { 420 | if (this.components.peerId.equals(from) && !this.emitSelf) { 421 | return 422 | } 423 | 424 | // Ensure the message is valid before processing it 425 | try { 426 | await this.validate(from, msg) 427 | } catch (err: any) { 428 | log('Message is invalid, dropping it. %O', err) 429 | return 430 | } 431 | 432 | if (this.subscriptions.has(msg.topic)) { 433 | const isFromSelf = this.components.peerId.equals(from) 434 | 435 | if (!isFromSelf || this.emitSelf) { 436 | super.dispatchEvent(new CustomEvent('message', { 437 | detail: msg 438 | })) 439 | } 440 | } 441 | 442 | await this.publishMessage(from, msg) 443 | } 444 | 445 | /** 446 | * The default msgID implementation 447 | * Child class can override this. 448 | */ 449 | getMsgId (msg: Message): Promise | Uint8Array { 450 | const signaturePolicy = this.globalSignaturePolicy 451 | switch (signaturePolicy) { 452 | case 'StrictSign': 453 | if (msg.type !== 'signed') { 454 | throw new CodeError('Message type should be "signed" when signature policy is StrictSign but it was not', codes.ERR_MISSING_SIGNATURE) 455 | } 456 | 457 | if (msg.sequenceNumber == null) { 458 | throw new CodeError('Need seqno when signature policy is StrictSign but it was missing', codes.ERR_MISSING_SEQNO) 459 | } 460 | 461 | if (msg.key == null) { 462 | throw new CodeError('Need key when signature policy is StrictSign but it was missing', codes.ERR_MISSING_KEY) 463 | } 464 | 465 | return msgId(msg.key, msg.sequenceNumber) 466 | case 'StrictNoSign': 467 | return noSignMsgId(msg.data) 468 | default: 469 | throw new CodeError('Cannot get message id: unhandled signature policy', codes.ERR_UNHANDLED_SIGNATURE_POLICY) 470 | } 471 | } 472 | 473 | /** 474 | * Whether to accept a message from a peer 475 | * Override to create a graylist 476 | */ 477 | acceptFrom (id: PeerId): boolean { 478 | return true 479 | } 480 | 481 | /** 482 | * Decode Uint8Array into an RPC object. 483 | * This can be override to use a custom router protobuf. 484 | */ 485 | abstract decodeRpc (bytes: Uint8Array | Uint8ArrayList): PubSubRPC 486 | 487 | /** 488 | * Encode RPC object into a Uint8Array. 489 | * This can be override to use a custom router protobuf. 490 | */ 491 | abstract encodeRpc (rpc: PubSubRPC): Uint8Array 492 | 493 | /** 494 | * Encode RPC object into a Uint8Array. 495 | * This can be override to use a custom router protobuf. 496 | */ 497 | abstract encodeMessage (rpc: PubSubRPCMessage): Uint8Array 498 | 499 | /** 500 | * Send an rpc object to a peer 501 | */ 502 | send (peer: PeerId, data: { messages?: Message[], subscriptions?: string[], subscribe?: boolean }): void { 503 | const { messages, subscriptions, subscribe } = data 504 | 505 | this.sendRpc(peer, { 506 | subscriptions: (subscriptions ?? []).map(str => ({ topic: str, subscribe: Boolean(subscribe) })), 507 | messages: (messages ?? []).map(toRpcMessage) 508 | }) 509 | } 510 | 511 | /** 512 | * Send an rpc object to a peer 513 | */ 514 | sendRpc (peer: PeerId, rpc: PubSubRPC): void { 515 | const peerStreams = this.peers.get(peer) 516 | 517 | if (peerStreams == null || !peerStreams.isWritable) { 518 | log.error('Cannot send RPC to %p as there is no open stream to it available', peer) 519 | 520 | return 521 | } 522 | 523 | peerStreams.write(this.encodeRpc(rpc)) 524 | } 525 | 526 | /** 527 | * Validates the given message. The signature will be checked for authenticity. 528 | * Throws an error on invalid messages 529 | */ 530 | async validate (from: PeerId, message: Message): Promise { // eslint-disable-line require-await 531 | const signaturePolicy = this.globalSignaturePolicy 532 | switch (signaturePolicy) { 533 | case 'StrictNoSign': 534 | if (message.type !== 'unsigned') { 535 | throw new CodeError('Message type should be "unsigned" when signature policy is StrictNoSign but it was not', codes.ERR_MISSING_SIGNATURE) 536 | } 537 | 538 | // @ts-expect-error should not be present 539 | if (message.signature != null) { 540 | throw new CodeError('StrictNoSigning: signature should not be present', codes.ERR_UNEXPECTED_SIGNATURE) 541 | } 542 | 543 | // @ts-expect-error should not be present 544 | if (message.key != null) { 545 | throw new CodeError('StrictNoSigning: key should not be present', codes.ERR_UNEXPECTED_KEY) 546 | } 547 | 548 | // @ts-expect-error should not be present 549 | if (message.sequenceNumber != null) { 550 | throw new CodeError('StrictNoSigning: seqno should not be present', codes.ERR_UNEXPECTED_SEQNO) 551 | } 552 | break 553 | case 'StrictSign': 554 | if (message.type !== 'signed') { 555 | throw new CodeError('Message type should be "signed" when signature policy is StrictSign but it was not', codes.ERR_MISSING_SIGNATURE) 556 | } 557 | 558 | if (message.signature == null) { 559 | throw new CodeError('StrictSigning: Signing required and no signature was present', codes.ERR_MISSING_SIGNATURE) 560 | } 561 | 562 | if (message.sequenceNumber == null) { 563 | throw new CodeError('StrictSigning: Signing required and no sequenceNumber was present', codes.ERR_MISSING_SEQNO) 564 | } 565 | 566 | if (!(await verifySignature(message, this.encodeMessage.bind(this)))) { 567 | throw new CodeError('StrictSigning: Invalid message signature', codes.ERR_INVALID_SIGNATURE) 568 | } 569 | 570 | break 571 | default: 572 | throw new CodeError('Cannot validate message: unhandled signature policy', codes.ERR_UNHANDLED_SIGNATURE_POLICY) 573 | } 574 | 575 | const validatorFn = this.topicValidators.get(message.topic) 576 | if (validatorFn != null) { 577 | const result = await validatorFn(from, message) 578 | if (result === TopicValidatorResult.Reject || result === TopicValidatorResult.Ignore) { 579 | throw new CodeError('Message validation failed', codes.ERR_TOPIC_VALIDATOR_REJECT) 580 | } 581 | } 582 | } 583 | 584 | /** 585 | * Normalizes the message and signs it, if signing is enabled. 586 | * Should be used by the routers to create the message to send. 587 | */ 588 | async buildMessage (message: { from: PeerId, topic: string, data: Uint8Array, sequenceNumber: bigint }): Promise { 589 | const signaturePolicy = this.globalSignaturePolicy 590 | switch (signaturePolicy) { 591 | case 'StrictSign': 592 | return signMessage(this.components.peerId, message, this.encodeMessage.bind(this)) 593 | case 'StrictNoSign': 594 | return Promise.resolve({ 595 | type: 'unsigned', 596 | ...message 597 | }) 598 | default: 599 | throw new CodeError('Cannot build message: unhandled signature policy', codes.ERR_UNHANDLED_SIGNATURE_POLICY) 600 | } 601 | } 602 | 603 | // API METHODS 604 | 605 | /** 606 | * Get a list of the peer-ids that are subscribed to one topic. 607 | */ 608 | getSubscribers (topic: string): PeerId[] { 609 | if (!this.started) { 610 | throw new CodeError('not started yet', 'ERR_NOT_STARTED_YET') 611 | } 612 | 613 | if (topic == null) { 614 | throw new CodeError('topic is required', 'ERR_NOT_VALID_TOPIC') 615 | } 616 | 617 | const peersInTopic = this.topics.get(topic.toString()) 618 | 619 | if (peersInTopic == null) { 620 | return [] 621 | } 622 | 623 | return Array.from(peersInTopic.values()) 624 | } 625 | 626 | /** 627 | * Publishes messages to all subscribed peers 628 | */ 629 | async publish (topic: string, data?: Uint8Array): Promise { 630 | if (!this.started) { 631 | throw new Error('Pubsub has not started') 632 | } 633 | 634 | const message = { 635 | from: this.components.peerId, 636 | topic, 637 | data: data ?? new Uint8Array(0), 638 | sequenceNumber: randomSeqno() 639 | } 640 | 641 | log('publish topic: %s from: %p data: %m', topic, message.from, message.data) 642 | 643 | const rpcMessage = await this.buildMessage(message) 644 | let emittedToSelf = false 645 | 646 | // dispatch the event if we are interested 647 | if (this.emitSelf) { 648 | if (this.subscriptions.has(topic)) { 649 | emittedToSelf = true 650 | super.dispatchEvent(new CustomEvent('message', { 651 | detail: rpcMessage 652 | })) 653 | } 654 | } 655 | 656 | // send to all the other peers 657 | const result = await this.publishMessage(this.components.peerId, rpcMessage) 658 | 659 | if (emittedToSelf) { 660 | result.recipients = [...result.recipients, this.components.peerId] 661 | } 662 | 663 | return result 664 | } 665 | 666 | /** 667 | * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation. 668 | * For example, a Floodsub implementation might simply publish each message to each topic for every peer. 669 | * 670 | * `sender` might be this peer, or we might be forwarding a message on behalf of another peer, in which case sender 671 | * is the peer we received the message from, which may not be the peer the message was created by. 672 | */ 673 | abstract publishMessage (sender: PeerId, message: Message): Promise 674 | 675 | /** 676 | * Subscribes to a given topic. 677 | */ 678 | subscribe (topic: string): void { 679 | if (!this.started) { 680 | throw new Error('Pubsub has not started') 681 | } 682 | 683 | log('subscribe to topic: %s', topic) 684 | 685 | if (!this.subscriptions.has(topic)) { 686 | this.subscriptions.add(topic) 687 | 688 | for (const peerId of this.peers.keys()) { 689 | this.send(peerId, { subscriptions: [topic], subscribe: true }) 690 | } 691 | } 692 | } 693 | 694 | /** 695 | * Unsubscribe from the given topic 696 | */ 697 | unsubscribe (topic: string): void { 698 | if (!this.started) { 699 | throw new Error('Pubsub is not started') 700 | } 701 | 702 | super.removeEventListener(topic) 703 | 704 | const wasSubscribed = this.subscriptions.has(topic) 705 | 706 | log('unsubscribe from %s - am subscribed %s', topic, wasSubscribed) 707 | 708 | if (wasSubscribed) { 709 | this.subscriptions.delete(topic) 710 | 711 | for (const peerId of this.peers.keys()) { 712 | this.send(peerId, { subscriptions: [topic], subscribe: false }) 713 | } 714 | } 715 | } 716 | 717 | /** 718 | * Get the list of topics which the peer is subscribed to. 719 | */ 720 | getTopics (): string[] { 721 | if (!this.started) { 722 | throw new Error('Pubsub is not started') 723 | } 724 | 725 | return Array.from(this.subscriptions) 726 | } 727 | 728 | getPeers (): PeerId[] { 729 | if (!this.started) { 730 | throw new Error('Pubsub is not started') 731 | } 732 | 733 | return Array.from(this.peers.keys()) 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [7.0.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v7.0.2...v7.0.3) (2023-06-27) 2 | 3 | 4 | ### Dependencies 5 | 6 | * **dev:** bump delay from 5.0.0 to 6.0.0 ([#144](https://github.com/libp2p/js-libp2p-pubsub/issues/144)) ([1364ce4](https://github.com/libp2p/js-libp2p-pubsub/commit/1364ce41815d3392cfca61169e113cc5414ac2d9)) 7 | 8 | ## [7.0.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v7.0.1...v7.0.2) (2023-06-27) 9 | 10 | 11 | ### Trivial Changes 12 | 13 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([ab88716](https://github.com/libp2p/js-libp2p-pubsub/commit/ab8871630d551841696cbcfaa94a2d2943601d74)) 14 | * Update .github/workflows/stale.yml [skip ci] ([f032696](https://github.com/libp2p/js-libp2p-pubsub/commit/f032696bbd33329985845341d2d1c6b7f9e8d23b)) 15 | 16 | 17 | ### Dependencies 18 | 19 | * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#146](https://github.com/libp2p/js-libp2p-pubsub/issues/146)) ([074e78b](https://github.com/libp2p/js-libp2p-pubsub/commit/074e78b1708190bc8f607bf0895fcfca77375d34)) 20 | 21 | ## [7.0.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v7.0.0...v7.0.1) (2023-04-19) 22 | 23 | 24 | ### Dependencies 25 | 26 | * bump abortable-iterator from 4.0.3 to 5.0.1 ([#137](https://github.com/libp2p/js-libp2p-pubsub/issues/137)) ([695ad25](https://github.com/libp2p/js-libp2p-pubsub/commit/695ad25b3b9b53ddbe646b507e7e7e3051b834cf)) 27 | 28 | ## [7.0.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.6...v7.0.0) (2023-04-18) 29 | 30 | 31 | ### ⚠ BREAKING CHANGES 32 | 33 | * update stream deps (#136) 34 | 35 | ### Dependencies 36 | 37 | * update stream deps ([#136](https://github.com/libp2p/js-libp2p-pubsub/issues/136)) ([8d6af79](https://github.com/libp2p/js-libp2p-pubsub/commit/8d6af79820700e2b7ffb4f11e939211cf2191f6e)) 38 | 39 | ## [6.0.6](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.5...v6.0.6) (2023-04-12) 40 | 41 | 42 | ### Dependencies 43 | 44 | * bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#135](https://github.com/libp2p/js-libp2p-pubsub/issues/135)) ([96b0c41](https://github.com/libp2p/js-libp2p-pubsub/commit/96b0c41ea3ecca6cf28ce8417ab0a5d782531658)) 45 | 46 | ## [6.0.5](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.4...v6.0.5) (2023-04-03) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * update project config ([5c7e7f9](https://github.com/libp2p/js-libp2p-pubsub/commit/5c7e7f9f0393c0b231108bd51a0b8c805712ca09)) 52 | 53 | 54 | ### Trivial Changes 55 | 56 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([b1c5590](https://github.com/libp2p/js-libp2p-pubsub/commit/b1c5590a9090adadc69d9f5b0ea508035e1c02f8)) 57 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([f83a2f7](https://github.com/libp2p/js-libp2p-pubsub/commit/f83a2f76235639aed4bd9ecd6114365330acd5ff)) 58 | 59 | 60 | ### Dependencies 61 | 62 | * bump it-length-prefixed from 8.x to 9.x ([#134](https://github.com/libp2p/js-libp2p-pubsub/issues/134)) ([ae3e688](https://github.com/libp2p/js-libp2p-pubsub/commit/ae3e6881aab3d79bf61ac1d3dab952f4a36cbd25)) 63 | * bump it-pipe from 2.0.5 to 3.0.0 ([#133](https://github.com/libp2p/js-libp2p-pubsub/issues/133)) ([cafd733](https://github.com/libp2p/js-libp2p-pubsub/commit/cafd7330727644b1074f57920099888385dbd81f)) 64 | 65 | ## [6.0.4](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.3...v6.0.4) (2023-02-22) 66 | 67 | 68 | ### Dependencies 69 | 70 | * **dev:** bump aegir from 37.12.1 to 38.1.6 ([#128](https://github.com/libp2p/js-libp2p-pubsub/issues/128)) ([7609545](https://github.com/libp2p/js-libp2p-pubsub/commit/7609545f732f94a1e52586b99f62f6c49d2b6c76)) 71 | 72 | ## [6.0.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.2...v6.0.3) (2023-02-22) 73 | 74 | 75 | ### Dependencies 76 | 77 | * **dev:** bump protons from 6.1.3 to 7.0.2 ([#124](https://github.com/libp2p/js-libp2p-pubsub/issues/124)) ([302763e](https://github.com/libp2p/js-libp2p-pubsub/commit/302763ecbc95b03051c071f821464d97825cb693)) 78 | 79 | ## [6.0.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.1...v6.0.2) (2023-02-22) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * get key from peer id if not specified in the message ([#129](https://github.com/libp2p/js-libp2p-pubsub/issues/129)) ([c183c70](https://github.com/libp2p/js-libp2p-pubsub/commit/c183c70f4d57af486e74fd2920eb1d0a878e6ab3)) 85 | 86 | 87 | ### Trivial Changes 88 | 89 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([9839b71](https://github.com/libp2p/js-libp2p-pubsub/commit/9839b71811a2467a9317700c9874b14a6da8759e)) 90 | 91 | ## [6.0.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v6.0.0...v6.0.1) (2023-01-31) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * allow `key` field to be unset ([#118](https://github.com/libp2p/js-libp2p-pubsub/issues/118)) ([2567a45](https://github.com/libp2p/js-libp2p-pubsub/commit/2567a454c9f4c91ab7d55e6a90c79e816d527a30)) 97 | 98 | 99 | ### Trivial Changes 100 | 101 | * replace err-code with CodeError ([#116](https://github.com/libp2p/js-libp2p-pubsub/issues/116)) ([e121e4b](https://github.com/libp2p/js-libp2p-pubsub/commit/e121e4b18ab9bca90ee4b596928a1de84fb412f7)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) 102 | 103 | ## [6.0.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v5.0.1...v6.0.0) (2023-01-06) 104 | 105 | 106 | ### ⚠ BREAKING CHANGES 107 | 108 | * update multiformats to v11 (#115) 109 | 110 | ### Bug Fixes 111 | 112 | * update multiformats to v11 ([#115](https://github.com/libp2p/js-libp2p-pubsub/issues/115)) ([148f554](https://github.com/libp2p/js-libp2p-pubsub/commit/148f5548001896869caca90c3cad9d8d363638f0)) 113 | 114 | ## [5.0.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v5.0.0...v5.0.1) (2022-12-16) 115 | 116 | 117 | ### Documentation 118 | 119 | * publish api docs ([#113](https://github.com/libp2p/js-libp2p-pubsub/issues/113)) ([bc20def](https://github.com/libp2p/js-libp2p-pubsub/commit/bc20defefafbe97defb64e18a7ae10527bff4ae6)) 120 | 121 | ## [5.0.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v4.0.1...v5.0.0) (2022-10-12) 122 | 123 | 124 | ### ⚠ BREAKING CHANGES 125 | 126 | * modules no longer implement `Initializable` instead switching to constructor injection 127 | 128 | ### Bug Fixes 129 | 130 | * remove @libp2p/components ([#106](https://github.com/libp2p/js-libp2p-pubsub/issues/106)) ([01707d7](https://github.com/libp2p/js-libp2p-pubsub/commit/01707d7dde5ff7d2f87115f9215d7a8a35d3d3f4)) 131 | 132 | ## [4.0.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v4.0.0...v4.0.1) (2022-10-11) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * update interface-pubsub and adjust topicValidator implementation ([#102](https://github.com/libp2p/js-libp2p-pubsub/issues/102)) ([f84d365](https://github.com/libp2p/js-libp2p-pubsub/commit/f84d36588a1096f349d68752db43247346163e82)) 138 | 139 | 140 | ### Documentation 141 | 142 | * update readme ([7a6f91d](https://github.com/libp2p/js-libp2p-pubsub/commit/7a6f91da1dd05c0039cebfe2a17742736be08c08)) 143 | 144 | ## [4.0.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.1.3...v4.0.0) (2022-10-07) 145 | 146 | 147 | ### ⚠ BREAKING CHANGES 148 | 149 | * bump @libp2p/components from 2.1.1 to 3.0.0 (#103) 150 | 151 | ### Dependencies 152 | 153 | * bump @libp2p/components from 2.1.1 to 3.0.0 ([#103](https://github.com/libp2p/js-libp2p-pubsub/issues/103)) ([fe407fe](https://github.com/libp2p/js-libp2p-pubsub/commit/fe407fe981c57aaef0b022b536a696c09faa72fd)) 154 | 155 | ## [3.1.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.1.2...v3.1.3) (2022-09-21) 156 | 157 | 158 | ### Trivial Changes 159 | 160 | * Update .github/workflows/stale.yml [skip ci] ([fcf5da9](https://github.com/libp2p/js-libp2p-pubsub/commit/fcf5da9f5038f3a544724adff2aa5559d31a82fe)) 161 | 162 | 163 | ### Dependencies 164 | 165 | * update @multiformats/multiaddr to 11.0.0 ([#101](https://github.com/libp2p/js-libp2p-pubsub/issues/101)) ([9524fa4](https://github.com/libp2p/js-libp2p-pubsub/commit/9524fa4e2d1935d7603bf2d2bfb09ac4f13675c2)) 166 | 167 | ## [3.1.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.1.1...v3.1.2) (2022-08-11) 168 | 169 | 170 | ### Dependencies 171 | 172 | * **dev:** update protons to 5.1.0 ([#98](https://github.com/libp2p/js-libp2p-pubsub/issues/98)) ([aa6dc45](https://github.com/libp2p/js-libp2p-pubsub/commit/aa6dc453dd2d5cdafa58b5c75571f9ec9f69d197)) 173 | 174 | ## [3.1.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.1.0...v3.1.1) (2022-08-10) 175 | 176 | 177 | ### Dependencies 178 | 179 | * update all deps ([#94](https://github.com/libp2p/js-libp2p-pubsub/issues/94)) ([5d5d788](https://github.com/libp2p/js-libp2p-pubsub/commit/5d5d78820c5feaca070ef504c83b730fd3b8b2d4)) 180 | 181 | ## [3.1.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.0.4...v3.1.0) (2022-08-03) 182 | 183 | 184 | ### Features 185 | 186 | * remove unnecessary direct dependency ([#92](https://github.com/libp2p/js-libp2p-pubsub/issues/92)) ([6d51017](https://github.com/libp2p/js-libp2p-pubsub/commit/6d510173d3708e32eb635aac6c3cf7c616d5be4c)) 187 | 188 | ## [3.0.4](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.0.3...v3.0.4) (2022-08-01) 189 | 190 | 191 | ### Trivial Changes 192 | 193 | * update project config ([#86](https://github.com/libp2p/js-libp2p-pubsub/issues/86)) ([3251829](https://github.com/libp2p/js-libp2p-pubsub/commit/3251829d4bb433fd26dc5cc9c8366c9a49d23e76)) 194 | 195 | 196 | ### Dependencies 197 | 198 | * update it-length-prefixed and uint8arraylists deps ([#91](https://github.com/libp2p/js-libp2p-pubsub/issues/91)) ([f295fce](https://github.com/libp2p/js-libp2p-pubsub/commit/f295fce10a32edb73789a6b08cd9ce9420bbb6a3)) 199 | 200 | ## [3.0.3](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.0.2...v3.0.3) (2022-06-30) 201 | 202 | 203 | ### Trivial Changes 204 | 205 | * **deps:** bump @libp2p/peer-collections from 1.0.3 to 2.0.0 ([#79](https://github.com/libp2p/js-libp2p-pubsub/issues/79)) ([c066676](https://github.com/libp2p/js-libp2p-pubsub/commit/c06667694053e4d6df1607cce7cffdbe9a3c25c0)) 206 | 207 | ## [3.0.2](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.0.1...v3.0.2) (2022-06-23) 208 | 209 | 210 | ### Bug Fixes 211 | 212 | * do not unsubscribe after publish ([#78](https://github.com/libp2p/js-libp2p-pubsub/issues/78)) ([760594e](https://github.com/libp2p/js-libp2p-pubsub/commit/760594e57224e38139a560c37747e52f9dd3e593)) 213 | 214 | ## [3.0.1](https://github.com/libp2p/js-libp2p-pubsub/compare/v3.0.0...v3.0.1) (2022-06-17) 215 | 216 | 217 | ### Bug Fixes 218 | 219 | * limit stream concurrency ([#77](https://github.com/libp2p/js-libp2p-pubsub/issues/77)) ([d4f1779](https://github.com/libp2p/js-libp2p-pubsub/commit/d4f1779b68e658211e7a50ba446ec479bb413d2b)) 220 | 221 | ## [3.0.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v2.0.0...v3.0.0) (2022-06-16) 222 | 223 | 224 | ### ⚠ BREAKING CHANGES 225 | 226 | * update to simpler connection api 227 | 228 | ### Trivial Changes 229 | 230 | * update deps ([#76](https://github.com/libp2p/js-libp2p-pubsub/issues/76)) ([50d1a5f](https://github.com/libp2p/js-libp2p-pubsub/commit/50d1a5fdb487f264f1f9da1facf96f4da6836649)) 231 | 232 | ## [2.0.0](https://github.com/libp2p/js-libp2p-pubsub/compare/v1.3.0...v2.0.0) (2022-06-15) 233 | 234 | 235 | ### ⚠ BREAKING CHANGES 236 | 237 | * uses new single-issue libp2p interface modules 238 | 239 | Co-authored-by: achingbrain 240 | 241 | ### Features 242 | 243 | * update to latest libp2p interfaces ([#74](https://github.com/libp2p/js-libp2p-pubsub/issues/74)) ([fe38340](https://github.com/libp2p/js-libp2p-pubsub/commit/fe38340715f37f6e976c526bf45e10d649b118dc)) 244 | 245 | ## [@libp2p/pubsub-v1.3.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.24...@libp2p/pubsub-v1.3.0) (2022-05-23) 246 | 247 | 248 | ### Features 249 | 250 | * expose utility methods to convert bigint to bytes and back ([#213](https://github.com/libp2p/js-libp2p-interfaces/issues/213)) ([3d2e59c](https://github.com/libp2p/js-libp2p-interfaces/commit/3d2e59c8fd8af5d618df904ae9d40518a13de547)) 251 | 252 | ## [@libp2p/pubsub-v1.2.24](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.23...@libp2p/pubsub-v1.2.24) (2022-05-20) 253 | 254 | 255 | ### Bug Fixes 256 | 257 | * update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) 258 | 259 | ## [@libp2p/pubsub-v1.2.23](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.22...@libp2p/pubsub-v1.2.23) (2022-05-10) 260 | 261 | 262 | ### Trivial Changes 263 | 264 | * **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) 265 | 266 | ## [@libp2p/pubsub-v1.2.22](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.21...@libp2p/pubsub-v1.2.22) (2022-05-10) 267 | 268 | 269 | ### Bug Fixes 270 | 271 | * regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) 272 | 273 | ## [@libp2p/pubsub-v1.2.21](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.20...@libp2p/pubsub-v1.2.21) (2022-05-04) 274 | 275 | 276 | ### Bug Fixes 277 | 278 | * move startable and events interfaces ([#209](https://github.com/libp2p/js-libp2p-interfaces/issues/209)) ([8ce8a08](https://github.com/libp2p/js-libp2p-interfaces/commit/8ce8a08c94b0738aa32da516558977b195ddd8ed)) 279 | 280 | ## [@libp2p/pubsub-v1.2.20](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.19...@libp2p/pubsub-v1.2.20) (2022-04-22) 281 | 282 | 283 | ### Bug Fixes 284 | 285 | * update pubsub interface in line with gossipsub ([#199](https://github.com/libp2p/js-libp2p-interfaces/issues/199)) ([3f55596](https://github.com/libp2p/js-libp2p-interfaces/commit/3f555965cddea3ef03e7217b755c82aa4107e093)) 286 | 287 | ## [@libp2p/pubsub-v1.2.19](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.18...@libp2p/pubsub-v1.2.19) (2022-04-21) 288 | 289 | 290 | ### Bug Fixes 291 | 292 | * test PubSub interface and not PubSubBaseProtocol ([#198](https://github.com/libp2p/js-libp2p-interfaces/issues/198)) ([96c15c9](https://github.com/libp2p/js-libp2p-interfaces/commit/96c15c9780821a3cb763e48854d64377bf562692)) 293 | 294 | ## [@libp2p/pubsub-v1.2.18](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.17...@libp2p/pubsub-v1.2.18) (2022-04-20) 295 | 296 | 297 | ### Bug Fixes 298 | 299 | * emit pubsub messages using 'message' event ([#197](https://github.com/libp2p/js-libp2p-interfaces/issues/197)) ([df9b685](https://github.com/libp2p/js-libp2p-interfaces/commit/df9b685cea30653109f2fa2cb5583a3bca7b09bb)) 300 | 301 | ## [@libp2p/pubsub-v1.2.17](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.16...@libp2p/pubsub-v1.2.17) (2022-04-19) 302 | 303 | 304 | ### Trivial Changes 305 | 306 | * remove extraneous readme ([#196](https://github.com/libp2p/js-libp2p-interfaces/issues/196)) ([ee1d00c](https://github.com/libp2p/js-libp2p-interfaces/commit/ee1d00cc209909836f12f17d62f1165f11689488)) 307 | 308 | ## [@libp2p/pubsub-v1.2.16](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.15...@libp2p/pubsub-v1.2.16) (2022-04-19) 309 | 310 | 311 | ### Bug Fixes 312 | 313 | * move dev deps to prod ([#195](https://github.com/libp2p/js-libp2p-interfaces/issues/195)) ([3e1ffc7](https://github.com/libp2p/js-libp2p-interfaces/commit/3e1ffc7b174e74be483943ad4e5fcab823ae3f6d)) 314 | 315 | ## [@libp2p/pubsub-v1.2.15](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.14...@libp2p/pubsub-v1.2.15) (2022-04-13) 316 | 317 | 318 | ### Bug Fixes 319 | 320 | * add keychain types, fix bigint types ([#193](https://github.com/libp2p/js-libp2p-interfaces/issues/193)) ([9ceadf9](https://github.com/libp2p/js-libp2p-interfaces/commit/9ceadf9d5c42a12d88d74ddd9140e34f7fa63537)) 321 | 322 | ## [@libp2p/pubsub-v1.2.14](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.13...@libp2p/pubsub-v1.2.14) (2022-04-08) 323 | 324 | 325 | ### Bug Fixes 326 | 327 | * swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) 328 | 329 | 330 | ### Trivial Changes 331 | 332 | * update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) 333 | 334 | ## [@libp2p/pubsub-v1.2.13](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.12...@libp2p/pubsub-v1.2.13) (2022-03-24) 335 | 336 | 337 | ### Bug Fixes 338 | 339 | * rename peer data to peer info ([#187](https://github.com/libp2p/js-libp2p-interfaces/issues/187)) ([dfea342](https://github.com/libp2p/js-libp2p-interfaces/commit/dfea3429bad57abde040397e4e7a58539829e9c2)) 340 | 341 | ## [@libp2p/pubsub-v1.2.12](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.11...@libp2p/pubsub-v1.2.12) (2022-03-21) 342 | 343 | 344 | ### Bug Fixes 345 | 346 | * handle empty pubsub messages ([#185](https://github.com/libp2p/js-libp2p-interfaces/issues/185)) ([0db8d84](https://github.com/libp2p/js-libp2p-interfaces/commit/0db8d84dd98ff6e99776c01a6b5bab404033bffa)) 347 | 348 | ## [@libp2p/pubsub-v1.2.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.10...@libp2p/pubsub-v1.2.11) (2022-03-20) 349 | 350 | 351 | ### Bug Fixes 352 | 353 | * update pubsub types ([#183](https://github.com/libp2p/js-libp2p-interfaces/issues/183)) ([7ef4baa](https://github.com/libp2p/js-libp2p-interfaces/commit/7ef4baad0fe30f783f3eecd5199ef92af08b7f57)) 354 | 355 | ## [@libp2p/pubsub-v1.2.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.9...@libp2p/pubsub-v1.2.10) (2022-03-15) 356 | 357 | 358 | ### Bug Fixes 359 | 360 | * simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) 361 | 362 | ## [@libp2p/pubsub-v1.2.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.8...@libp2p/pubsub-v1.2.9) (2022-02-27) 363 | 364 | 365 | ### Bug Fixes 366 | 367 | * rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) 368 | 369 | ## [@libp2p/pubsub-v1.2.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.7...@libp2p/pubsub-v1.2.8) (2022-02-27) 370 | 371 | 372 | ### Bug Fixes 373 | 374 | * update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) 375 | 376 | ## [@libp2p/pubsub-v1.2.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.6...@libp2p/pubsub-v1.2.7) (2022-02-18) 377 | 378 | 379 | ### Bug Fixes 380 | 381 | * simpler pubsub ([#172](https://github.com/libp2p/js-libp2p-interfaces/issues/172)) ([98715ed](https://github.com/libp2p/js-libp2p-interfaces/commit/98715ed73183b32e4fda3d878a462389548358d9)) 382 | 383 | ## [@libp2p/pubsub-v1.2.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.5...@libp2p/pubsub-v1.2.6) (2022-02-17) 384 | 385 | 386 | ### Bug Fixes 387 | 388 | * update deps ([#171](https://github.com/libp2p/js-libp2p-interfaces/issues/171)) ([d0d2564](https://github.com/libp2p/js-libp2p-interfaces/commit/d0d2564a84a0722ab587a3aa6ec01e222442b100)) 389 | 390 | ## [@libp2p/pubsub-v1.2.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.4...@libp2p/pubsub-v1.2.5) (2022-02-17) 391 | 392 | 393 | ### Bug Fixes 394 | 395 | * add multistream-select and update pubsub types ([#170](https://github.com/libp2p/js-libp2p-interfaces/issues/170)) ([b9ecb2b](https://github.com/libp2p/js-libp2p-interfaces/commit/b9ecb2bee8f2abc0c41bfcf7bf2025894e37ddc2)) 396 | 397 | ## [@libp2p/pubsub-v1.2.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.3...@libp2p/pubsub-v1.2.4) (2022-02-12) 398 | 399 | 400 | ### Bug Fixes 401 | 402 | * hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) 403 | 404 | ## [@libp2p/pubsub-v1.2.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.2...@libp2p/pubsub-v1.2.3) (2022-02-11) 405 | 406 | 407 | ### Bug Fixes 408 | 409 | * simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) 410 | 411 | ## [@libp2p/pubsub-v1.2.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.1...@libp2p/pubsub-v1.2.2) (2022-02-10) 412 | 413 | 414 | ### Bug Fixes 415 | 416 | * make registrar simpler ([#163](https://github.com/libp2p/js-libp2p-interfaces/issues/163)) ([d122f3d](https://github.com/libp2p/js-libp2p-interfaces/commit/d122f3daaccc04039d90814960da92b513265644)) 417 | 418 | ## [@libp2p/pubsub-v1.2.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.2.0...@libp2p/pubsub-v1.2.1) (2022-02-10) 419 | 420 | 421 | ### Bug Fixes 422 | 423 | * remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) 424 | 425 | ## [@libp2p/pubsub-v1.2.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.1.0...@libp2p/pubsub-v1.2.0) (2022-02-09) 426 | 427 | 428 | ### Features 429 | 430 | * add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) 431 | 432 | ## [@libp2p/pubsub-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.6...@libp2p/pubsub-v1.1.0) (2022-02-07) 433 | 434 | 435 | ### Features 436 | 437 | * add logger package ([#158](https://github.com/libp2p/js-libp2p-interfaces/issues/158)) ([f327cd2](https://github.com/libp2p/js-libp2p-interfaces/commit/f327cd24825d9ce2f45a02fdb9b47c9735c847e0)) 438 | 439 | ## [@libp2p/pubsub-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.5...@libp2p/pubsub-v1.0.6) (2022-02-05) 440 | 441 | 442 | ### Bug Fixes 443 | 444 | * fix muxer tests ([#157](https://github.com/libp2p/js-libp2p-interfaces/issues/157)) ([7233c44](https://github.com/libp2p/js-libp2p-interfaces/commit/7233c4438479dff56a682f45209ef7a938d63857)) 445 | 446 | ## [@libp2p/pubsub-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.4...@libp2p/pubsub-v1.0.5) (2022-01-15) 447 | 448 | 449 | ### Bug Fixes 450 | 451 | * remove abort controller dep ([#151](https://github.com/libp2p/js-libp2p-interfaces/issues/151)) ([518bce1](https://github.com/libp2p/js-libp2p-interfaces/commit/518bce1f9bd1f8b2922338e0c65c9934af7da3af)) 452 | 453 | ## [@libp2p/pubsub-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.3...@libp2p/pubsub-v1.0.4) (2022-01-15) 454 | 455 | 456 | ### Trivial Changes 457 | 458 | * update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) 459 | 460 | ## [@libp2p/pubsub-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.2...@libp2p/pubsub-v1.0.3) (2022-01-14) 461 | 462 | 463 | ### Bug Fixes 464 | 465 | * update it-* deps to ts versions ([#148](https://github.com/libp2p/js-libp2p-interfaces/issues/148)) ([7a6fdd7](https://github.com/libp2p/js-libp2p-interfaces/commit/7a6fdd7622ce2870b89dbb849ab421d0dd714b43)) 466 | 467 | ## [@libp2p/pubsub-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/pubsub-v1.0.1...@libp2p/pubsub-v1.0.2) (2022-01-08) 468 | 469 | 470 | ### Trivial Changes 471 | 472 | * add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) 473 | * update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) 474 | 475 | # Change Log 476 | 477 | All notable changes to this project will be documented in this file. 478 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 479 | 480 | # 0.2.0 (2022-01-04) 481 | 482 | 483 | ### chore 484 | 485 | * update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) 486 | 487 | 488 | ### Features 489 | 490 | * add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) 491 | * simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) 492 | * split out code, convert to typescript ([#111](https://github.com/libp2p/js-libp2p-interfaces/issues/111)) ([e174bba](https://github.com/libp2p/js-libp2p-interfaces/commit/e174bba889388269b806643c79a6b53c8d6a0f8c)), closes [#110](https://github.com/libp2p/js-libp2p-interfaces/issues/110) [#101](https://github.com/libp2p/js-libp2p-interfaces/issues/101) 493 | * update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) 494 | 495 | 496 | ### BREAKING CHANGES 497 | 498 | * requires node 15+ 499 | * not all fields from concrete classes have been added to the interfaces, some adjustment may be necessary as this gets rolled out 500 | 501 | 502 | 503 | 504 | 505 | ## [0.9.1](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-pubsub@0.9.0...libp2p-pubsub@0.9.1) (2022-01-02) 506 | 507 | **Note:** Version bump only for package libp2p-pubsub 508 | 509 | 510 | 511 | 512 | 513 | # [0.9.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-pubsub@0.8.0...libp2p-pubsub@0.9.0) (2022-01-02) 514 | 515 | 516 | ### Features 517 | 518 | * simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) 519 | 520 | 521 | 522 | 523 | 524 | # [0.8.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-pubsub@0.7.0...libp2p-pubsub@0.8.0) (2021-12-02) 525 | 526 | 527 | ### chore 528 | 529 | * update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) 530 | 531 | 532 | ### BREAKING CHANGES 533 | 534 | * requires node 15+ 535 | 536 | 537 | 538 | 539 | 540 | # 0.7.0 (2021-11-22) 541 | 542 | 543 | ### Features 544 | 545 | * split out code, convert to typescript ([#111](https://github.com/libp2p/js-libp2p-interfaces/issues/111)) ([e174bba](https://github.com/libp2p/js-libp2p-interfaces/commit/e174bba889388269b806643c79a6b53c8d6a0f8c)), closes [#110](https://github.com/libp2p/js-libp2p-interfaces/issues/110) [#101](https://github.com/libp2p/js-libp2p-interfaces/issues/101) 546 | 547 | 548 | ### BREAKING CHANGES 549 | 550 | * not all fields from concrete classes have been added to the interfaces, some adjustment may be necessary as this gets rolled out 551 | --------------------------------------------------------------------------------