diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 988cb148c..6c22e94b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,8 +16,8 @@ jobs: with: node-version: 16 - run: npm install - - run: npm run lint - run: npm run build + - run: npm run lint - run: npm run dep-check test-node: needs: check diff --git a/package.json b/package.json index 53db97ea6..395803c93 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "js-libp2p-interfaces", "version": "1.0.0", "description": "Interfaces for JS Libp2p", - "leadMaintainer": "Jacob Heun ", "private": true, "scripts": { "reset": "lerna run clean && rimraf ./node_modules ./package-lock.json packages/*/node_modules packages/*/package-lock.json packages/*/dist", diff --git a/packages/compliance-tests/package.json b/packages/compliance-tests/package.json deleted file mode 100644 index 107b3834d..000000000 --- a/packages/compliance-tests/package.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "name": "libp2p-interfaces-compliance-tests", - "version": "1.1.2", - "description": "Compliance tests for JS Libp2p interfaces", - "files": [ - "src", - "dist" - ], - "eslintConfig": { - "extends": "ipfs" - }, - "scripts": { - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "test": "aegir test", - "test:node": "aegir test --target node", - "test:browser": "aegir test --target browser", - "release": "aegir release --no-test", - "release-minor": "aegir release --type minor --no-test", - "release-major": "aegir release --type major --no-test" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/libp2p/js-libp2p-interfaces.git" - }, - "keywords": [ - "libp2p", - "interface" - ], - "author": "", - "license": "MIT", - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-interfaces/issues" - }, - "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/compliance-tests#readme#readme", - "dependencies": { - "abort-controller": "^3.0.0", - "abortable-iterator": "^3.0.0", - "aegir": "^35.0.1", - "chai": "^4.3.4", - "chai-checkmark": "^1.0.1", - "delay": "^5.0.0", - "it-goodbye": "^3.0.0", - "it-pair": "^1.0.0", - "it-pipe": "^1.1.0", - "libp2p-crypto": "^0.19.5", - "libp2p-interfaces": "^1.2.0", - "multiaddr": "^10.0.0", - "multiformats": "^9.4.9", - "p-defer": "^3.0.0", - "p-limit": "^3.1.0", - "p-wait-for": "^3.2.0", - "peer-id": "^0.15.0", - "sinon": "^11.1.1", - "streaming-iterables": "^6.0.0", - "uint8arrays": "^3.0.0" - }, - "devDependencies": { - "it-handshake": "^2.0.0" - }, - "contributors": [ - "Alan Shaw ", - "David Dias ", - "David Dias ", - "Dmitriy Ryajov ", - "Friedel Ziegelmayer ", - "Greg Zuro ", - "Jacob Heun ", - "Jacob Heun ", - "James Ray <16969914+jamesray1@users.noreply.github.com>", - "Jeffrey Hulten ", - "João Santos ", - "Maciej Krüger ", - "Matt Joiner ", - "Mike Goelzer ", - "Patrik Wallstrom ", - "Pau Ramon Revilla ", - "Richard Littauer ", - "Sathya Narrayanan ", - "Vasco Santos ", - "Vasco Santos ", - "dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>", - "dirkmc ", - "dmitriy ryajov ", - "greenkeeperio-bot ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ " - ] -} diff --git a/packages/compliance-tests/src/connection/index.js b/packages/compliance-tests/src/connection/index.js deleted file mode 100644 index f159b5b22..000000000 --- a/packages/compliance-tests/src/connection/index.js +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ - -'use strict' - -const connectionSuite = require('./connection') - -module.exports = (test) => { - connectionSuite(test) -} diff --git a/packages/compliance-tests/src/pubsub/emit-self.js b/packages/compliance-tests/src/pubsub/emit-self.js deleted file mode 100644 index 2659589ef..000000000 --- a/packages/compliance-tests/src/pubsub/emit-self.js +++ /dev/null @@ -1,69 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const topic = 'foo' -const data = uint8ArrayFromString('bar') -const shouldNotHappen = (_) => expect.fail() - -module.exports = (common) => { - describe('emit self', () => { - let pubsub - - describe('enabled', () => { - before(async () => { - [pubsub] = await common.setup(1, { emitSelf: true }) - }) - - before(() => { - pubsub.start() - pubsub.subscribe(topic) - }) - - after(async () => { - sinon.restore() - pubsub && pubsub.stop() - await common.teardown() - }) - - it('should emit to self on publish', () => { - const promise = new Promise((resolve) => pubsub.once(topic, resolve)) - - pubsub.publish(topic, data) - - return promise - }) - }) - - describe('disabled', () => { - before(async () => { - [pubsub] = await common.setup(1, { emitSelf: false }) - }) - - before(() => { - pubsub.start() - pubsub.subscribe(topic) - }) - - after(async () => { - sinon.restore() - pubsub && pubsub.stop() - await common.teardown() - }) - - it('should not emit to self on publish', () => { - pubsub.once(topic, (m) => shouldNotHappen) - - pubsub.publish(topic, data) - - // Wait 1 second to guarantee that self is not noticed - return new Promise((resolve) => setTimeout(resolve, 1000)) - }) - }) - }) -} diff --git a/packages/compliance-tests/src/pubsub/index.js b/packages/compliance-tests/src/pubsub/index.js deleted file mode 100644 index df2aed150..000000000 --- a/packages/compliance-tests/src/pubsub/index.js +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const apiTest = require('./api') -const emitSelfTest = require('./emit-self') -const messagesTest = require('./messages') -const connectionHandlersTest = require('./connection-handlers') -const twoNodesTest = require('./two-nodes') -const multipleNodesTest = require('./multiple-nodes') - -module.exports = (common) => { - describe('interface-pubsub compliance tests', () => { - apiTest(common) - emitSelfTest(common) - messagesTest(common) - connectionHandlersTest(common) - twoNodesTest(common) - multipleNodesTest(common) - }) -} diff --git a/packages/compliance-tests/src/pubsub/utils.js b/packages/compliance-tests/src/pubsub/utils.js deleted file mode 100644 index 935c220ea..000000000 --- a/packages/compliance-tests/src/pubsub/utils.js +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck interface tests -'use strict' - -const { expect } = require('aegir/utils/chai') - -exports.first = (map) => map.values().next().value - -exports.expectSet = (set, subs) => { - expect(Array.from(set.values())).to.eql(subs) -} diff --git a/packages/compliance-tests/src/stream-muxer/base-test.js b/packages/compliance-tests/src/stream-muxer/base-test.js deleted file mode 100644 index ce983f038..000000000 --- a/packages/compliance-tests/src/stream-muxer/base-test.js +++ /dev/null @@ -1,155 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('chai-checkmark')) -const { expect } = chai -const pair = require('it-pair/duplex') -const { pipe } = require('it-pipe') -const { collect, map, consume } = require('streaming-iterables') - -function close (stream) { - return pipe([], stream, consume) -} - -async function closeAndWait (stream) { - await close(stream) - expect(true).to.be.true.mark() -} - -/** - * A tick is considered valid if it happened between now - * and `ms` milliseconds ago - * - * @param {number} date - Time in ticks - * @param {number} ms - max milliseconds that should have expired - * @returns {boolean} - */ -function isValidTick (date, ms = 5000) { - const now = Date.now() - if (date > now - ms && date <= now) return true - return false -} - -module.exports = (common) => { - describe('base', () => { - let Muxer - - beforeEach(async () => { - Muxer = await common.setup() - }) - - it('Open a stream from the dialer', (done) => { - const p = pair() - const dialer = new Muxer() - - const listener = new Muxer({ - onStream: stream => { - expect(stream).to.exist.mark() // 1st check - expect(isValidTick(stream.timeline.open)).to.equal(true) - // Make sure the stream is being tracked - expect(listener.streams).to.include(stream) - close(stream) - }, - onStreamEnd: stream => { - expect(stream).to.exist.mark() // 2nd check - expect(listener.streams).to.not.include(stream) - // Make sure the stream is removed from tracking - expect(isValidTick(stream.timeline.close)).to.equal(true) - } - }) - - pipe(p[0], dialer, p[0]) - pipe(p[1], listener, p[1]) - - expect(3).checks(() => { - // ensure we have no streams left - expect(dialer.streams).to.have.length(0) - expect(listener.streams).to.have.length(0) - done() - }) - - const conn = dialer.newStream() - expect(dialer.streams).to.include(conn) - expect(isValidTick(conn.timeline.open)).to.equal(true) - - closeAndWait(conn) // 3rd check - }) - - it('Open a stream from the listener', (done) => { - const p = pair() - const dialer = new Muxer(stream => { - expect(stream).to.exist.mark() - expect(isValidTick(stream.timeline.open)).to.equal(true) - closeAndWait(stream) - }) - const listener = new Muxer() - - pipe(p[0], dialer, p[0]) - pipe(p[1], listener, p[1]) - - expect(3).check(done) - - const conn = listener.newStream() - expect(listener.streams).to.include(conn) - expect(isValidTick(conn.timeline.open)).to.equal(true) - - closeAndWait(conn) - }) - - it('Open a stream on both sides', (done) => { - const p = pair() - const dialer = new Muxer(stream => { - expect(stream).to.exist.mark() - closeAndWait(stream) - }) - const listener = new Muxer(stream => { - expect(stream).to.exist.mark() - closeAndWait(stream) - }) - - pipe(p[0], dialer, p[0]) - pipe(p[1], listener, p[1]) - - expect(6).check(done) - - const listenerConn = listener.newStream() - const dialerConn = dialer.newStream() - - closeAndWait(dialerConn) - closeAndWait(listenerConn) - }) - - it('Open a stream on one side, write, open a stream on the other side', (done) => { - const toString = map(c => c.slice().toString()) - const p = pair() - const dialer = new Muxer() - const listener = new Muxer(stream => { - pipe(stream, toString, collect).then(chunks => { - expect(chunks).to.be.eql(['hey']).mark() - }) - - dialer.onStream = onDialerStream - - const listenerConn = listener.newStream() - - pipe(['hello'], listenerConn) - - async function onDialerStream (stream) { - const chunks = await pipe(stream, toString, collect) - expect(chunks).to.be.eql(['hello']).mark() - } - }) - - pipe(p[0], dialer, p[0]) - pipe(p[1], listener, p[1]) - - expect(2).check(done) - - const dialerConn = dialer.newStream() - - pipe(['hey'], dialerConn) - }) - }) -} diff --git a/packages/compliance-tests/src/stream-muxer/index.js b/packages/compliance-tests/src/stream-muxer/index.js deleted file mode 100644 index 98f76e5d8..000000000 --- a/packages/compliance-tests/src/stream-muxer/index.js +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const baseTest = require('./base-test') -const closeTest = require('./close-test') -const stressTest = require('./stress-test') -const megaStressTest = require('./mega-stress-test') - -module.exports = (common) => { - describe('interface-stream-muxer', () => { - baseTest(common) - closeTest(common) - stressTest(common) - megaStressTest(common) - }) -} diff --git a/packages/compliance-tests/src/stream-muxer/mega-stress-test.js b/packages/compliance-tests/src/stream-muxer/mega-stress-test.js deleted file mode 100644 index 8ccbc62da..000000000 --- a/packages/compliance-tests/src/stream-muxer/mega-stress-test.js +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const spawn = require('./spawner') - -module.exports = (common) => { - describe.skip('mega stress test', function () { - let Muxer - - beforeEach(async () => { - Muxer = await common.setup() - }) - - it('10,000 streams with 10,000 msg', () => spawn(Muxer, 10000, 10000, 5000)) - }) -} diff --git a/packages/compliance-tests/src/stream-muxer/spawner.js b/packages/compliance-tests/src/stream-muxer/spawner.js deleted file mode 100644 index 8d2563975..000000000 --- a/packages/compliance-tests/src/stream-muxer/spawner.js +++ /dev/null @@ -1,89 +0,0 @@ -// @ts-nocheck interface tests -'use strict' - -const { expect } = require('aegir/utils/chai') -const pair = require('it-pair/duplex') -const { pipe } = require('it-pipe') - -const pLimit = require('p-limit') -const { collect, tap, consume } = require('streaming-iterables') - -module.exports = async (Muxer, nStreams, nMsg, limit) => { - const [dialerSocket, listenerSocket] = pair() - const { check, done } = marker((4 * nStreams) + (nStreams * nMsg)) - - const msg = 'simple msg' - - const listener = new Muxer(async stream => { - expect(stream).to.exist // eslint-disable-line - check() - - await pipe( - stream, - tap(chunk => check()), - consume - ) - - check() - pipe([], stream) - }) - - const dialer = new Muxer() - - pipe(listenerSocket, listener, listenerSocket) - pipe(dialerSocket, dialer, dialerSocket) - - const spawnStream = async n => { - const stream = dialer.newStream() - expect(stream).to.exist // eslint-disable-line - check() - - const res = await pipe( - (function * () { - for (let i = 0; i < nMsg; i++) { - // console.log('n', n, 'msg', i) - yield new Promise(resolve => resolve(msg)) - } - })(), - stream, - collect - ) - - expect(res).to.be.eql([]) - check() - } - - const limiter = pLimit(limit || Infinity) - - await Promise.all( - Array.from(Array(nStreams), (_, i) => limiter(() => spawnStream(i))) - ) - - return done -} - -function marker (n) { - /** @type {Function} */ - let check - let i = 0 - - /** @type {Promise} */ - const done = new Promise((resolve, reject) => { - check = err => { - i++ - - if (err) { - /* eslint-disable-next-line */ - console.error('Failed after %s iterations', i) - return reject(err) - } - - if (i === n) { - resolve() - } - } - }) - - // @ts-ignore - TS can't see that assignement occured - return { check, done } -} diff --git a/packages/compliance-tests/src/stream-muxer/stress-test.js b/packages/compliance-tests/src/stream-muxer/stress-test.js deleted file mode 100644 index cd2199535..000000000 --- a/packages/compliance-tests/src/stream-muxer/stress-test.js +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const spawn = require('./spawner') - -module.exports = (common) => { - describe('stress test', () => { - let Muxer - - beforeEach(async () => { - Muxer = await common.setup() - }) - - it('1 stream with 1 msg', () => spawn(Muxer, 1, 1)) - it('1 stream with 10 msg', () => spawn(Muxer, 1, 10)) - it('1 stream with 100 msg', () => spawn(Muxer, 1, 100)) - it('10 streams with 1 msg', () => spawn(Muxer, 10, 1)) - it('10 streams with 10 msg', () => spawn(Muxer, 10, 10)) - it('10 streams with 100 msg', () => spawn(Muxer, 10, 100)) - it('100 streams with 1 msg', () => spawn(Muxer, 100, 1)) - it('100 streams with 10 msg', () => spawn(Muxer, 100, 10)) - it('100 streams with 100 msg', () => spawn(Muxer, 100, 100)) - it('1000 streams with 1 msg', () => spawn(Muxer, 1000, 1)) - it('1000 streams with 10 msg', () => spawn(Muxer, 1000, 10)) - it('1000 streams with 100 msg', function () { - return spawn(Muxer, 1000, 100) - }) - }) -} diff --git a/packages/compliance-tests/src/topology/topology.js b/packages/compliance-tests/src/topology/topology.js deleted file mode 100644 index 429f45c1c..000000000 --- a/packages/compliance-tests/src/topology/topology.js +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const PeerId = require('peer-id') -const peers = require('../utils/peers') - -module.exports = (test) => { - describe('topology', () => { - let topology, id - - beforeEach(async () => { - topology = await test.setup() - if (!topology) throw new Error('missing multicodec topology') - - id = await PeerId.createFromJSON(peers[0]) - }) - - afterEach(async () => { - sinon.restore() - await test.teardown() - }) - - it('should have properties set', () => { - expect(topology.min).to.exist() - expect(topology.max).to.exist() - expect(topology._onConnect).to.exist() - expect(topology._onDisconnect).to.exist() - expect(topology.peers).to.exist() - }) - - it('should trigger "onDisconnect" on peer disconnected', () => { - sinon.spy(topology, '_onDisconnect') - topology.disconnect(id) - - expect(topology._onDisconnect.callCount).to.equal(1) - }) - }) -} diff --git a/packages/compliance-tests/src/transport/filter-test.js b/packages/compliance-tests/src/transport/filter-test.js deleted file mode 100644 index 37c3d2d5c..000000000 --- a/packages/compliance-tests/src/transport/filter-test.js +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') - -module.exports = (common) => { - const upgrader = { - _upgrade (multiaddrConnection) { - return multiaddrConnection - }, - upgradeOutbound (multiaddrConnection) { - return upgrader._upgrade(multiaddrConnection) - }, - upgradeInbound (multiaddrConnection) { - return upgrader._upgrade(multiaddrConnection) - } - } - - describe('filter', () => { - let addrs - let transport - - before(async () => { - ({ addrs, transport } = await common.setup({ upgrader })) - }) - - after(() => common.teardown && common.teardown()) - - it('filters addresses', () => { - const filteredAddrs = transport.filter(addrs) - expect(filteredAddrs).to.eql(addrs) - }) - }) -} diff --git a/packages/compliance-tests/src/transport/index.js b/packages/compliance-tests/src/transport/index.js deleted file mode 100644 index 35b82726e..000000000 --- a/packages/compliance-tests/src/transport/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const dial = require('./dial-test') -const listen = require('./listen-test') -const filter = require('./filter-test') - -module.exports = (common) => { - describe('interface-transport', () => { - dial(common) - listen(common) - filter(common) - }) -} diff --git a/packages/compliance-tests/src/transport/utils/index.js b/packages/compliance-tests/src/transport/utils/index.js deleted file mode 100644 index 532395a4f..000000000 --- a/packages/compliance-tests/src/transport/utils/index.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict' - -module.exports = { - /** - * A tick is considered valid if it happened between now - * and `ms` milliseconds ago - * - * @param {number} date - Time in ticks - * @param {number} ms - max milliseconds that should have expired - * @returns {boolean} - */ - isValidTick: function isValidTick (date, ms = 5000) { - const now = Date.now() - if (date > now - ms && date <= now) return true - return false - } -} diff --git a/packages/compliance-tests/test/crypto/index.spec.js b/packages/compliance-tests/test/crypto/index.spec.js deleted file mode 100644 index e14cd3e15..000000000 --- a/packages/compliance-tests/test/crypto/index.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const tests = require('../../src/crypto') -const mockCrypto = require('./mock-crypto') - -describe('compliance tests', () => { - tests({ - setup () { - return mockCrypto - } - }) -}) diff --git a/packages/compliance-tests/test/crypto/mock-crypto.js b/packages/compliance-tests/test/crypto/mock-crypto.js deleted file mode 100644 index 77fb5aa46..000000000 --- a/packages/compliance-tests/test/crypto/mock-crypto.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' - -const PeerId = require('peer-id') -const handshake = require('it-handshake') -const duplexPair = require('it-pair/duplex') -const pipe = require('it-pipe') -const { UnexpectedPeerError } = require('libp2p-interfaces/src/crypto/errors') - -// A basic transform that does nothing to the data -const transform = () => { - return (source) => (async function * () { - for await (const chunk of source) { - yield chunk - } - })() -} - -module.exports = { - protocol: 'insecure', - secureInbound: async (localPeer, duplex, expectedPeer) => { - // 1. Perform a basic handshake. - const shake = handshake(duplex) - shake.write(localPeer.id) - const remoteId = await shake.read() - const remotePeer = new PeerId(remoteId.slice()) - shake.rest() - - if (expectedPeer && expectedPeer.id !== remotePeer.id) { - throw new UnexpectedPeerError() - } - - // 2. Create your encryption box/unbox wrapper - const wrapper = duplexPair() - const encrypt = transform() // Use transform iterables to modify data - const decrypt = transform() - - pipe( - wrapper[0], // We write to wrapper - encrypt, // The data is encrypted - shake.stream, // It goes to the remote peer - decrypt, // Decrypt the incoming data - wrapper[0] // Pipe to the wrapper - ) - - return { - conn: wrapper[1], - remotePeer - } - }, - secureOutbound: async (localPeer, duplex, remotePeer) => { - // 1. Perform a basic handshake. - const shake = handshake(duplex) - shake.write(localPeer.id) - const remoteId = await shake.read() - shake.rest() - - // 2. Create your encryption box/unbox wrapper - const wrapper = duplexPair() - const encrypt = transform() - const decrypt = transform() - - pipe( - wrapper[0], // We write to wrapper - encrypt, // The data is encrypted - shake.stream, // It goes to the remote peer - decrypt, // Decrypt the incoming data - wrapper[0] // Pipe to the wrapper - ) - - return { - conn: wrapper[1], - remotePeer: new PeerId(remoteId.slice()) - } - } -} diff --git a/packages/compliance-tests/test/peer-discovery/mock-discovery.js b/packages/compliance-tests/test/peer-discovery/mock-discovery.js deleted file mode 100644 index 12d8d3cbe..000000000 --- a/packages/compliance-tests/test/peer-discovery/mock-discovery.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -const { EventEmitter } = require('events') - -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') - -/** - * Emits 'peer' events on discovery. - */ -class MockDiscovery extends EventEmitter { - /** - * Constructs a new Bootstrap. - * - * @param {Object} options - * @param {number} options.discoveryDelay - the delay to find a peer (in milli-seconds) - */ - constructor (options = {}) { - super() - - this.options = options - this._isRunning = false - this._timer = null - } - - start () { - this._isRunning = true - this._discoverPeer() - } - - stop () { - clearTimeout(this._timer) - this._isRunning = false - } - - async _discoverPeer () { - if (!this._isRunning) return - - const peerId = await PeerId.create({ bits: 512 }) - - this._timer = setTimeout(() => { - this.emit('peer', { - id: peerId, - multiaddrs: [new Multiaddr('/ip4/127.0.0.1/tcp/8000')] - }) - }, this.options.discoveryDelay || 1000) - } -} - -module.exports = MockDiscovery diff --git a/packages/compliance-tests/test/topology/mock-peer-store.js b/packages/compliance-tests/test/topology/mock-peer-store.js deleted file mode 100644 index 637d71fa8..000000000 --- a/packages/compliance-tests/test/topology/mock-peer-store.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const { EventEmitter } = require('events') - -class MockPeerStore extends EventEmitter { - constructor (peers) { - super() - this.peers = peers - this.protoBook = { - get: () => {} - } - } - - get (peerId) { - return this.peers.get(peerId.toB58String()) - } -} - -module.exports = MockPeerStore diff --git a/packages/compliance-tests/test/topology/multicodec-topology.spec.js b/packages/compliance-tests/test/topology/multicodec-topology.spec.js deleted file mode 100644 index 699782bec..000000000 --- a/packages/compliance-tests/test/topology/multicodec-topology.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { EventEmitter } = require('events') - -const tests = require('../../src/topology/multicodec-topology') -const MulticodecTopology = require('libp2p-interfaces/src/topology/multicodec-topology') -const MockPeerStore = require('./mock-peer-store') - -describe('multicodec topology compliance tests', () => { - tests({ - setup (properties, registrar) { - const multicodecs = ['/echo/1.0.0'] - const handlers = { - onConnect: () => { }, - onDisconnect: () => { } - } - - const topology = new MulticodecTopology({ - multicodecs, - handlers, - ...properties - }) - - if (!registrar) { - const peers = new Map() - const peerStore = new MockPeerStore(peers) - const connectionManager = new EventEmitter() - - registrar = { - peerStore, - connectionManager, - getConnection: () => { } - } - } - - topology.registrar = registrar - - return topology - }, - teardown () { - // cleanup resources created by setup() - } - }) -}) diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json deleted file mode 100644 index 71d62aebe..000000000 --- a/packages/interfaces/package.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "name": "libp2p-interfaces", - "version": "1.2.0", - "description": "Interfaces for JS Libp2p", - "leadMaintainer": "Jacob Heun ", - "main": "src/index.js", - "files": [ - "src", - "types", - "dist" - ], - "types": "dist/src/index.d.ts", - "typesVersions": { - "*": { - "src/*": [ - "dist/src/*", - "dist/src/*/index" - ] - } - }, - "eslintConfig": { - "extends": "ipfs", - "ignorePatterns": [ - "**/*.d.ts" - ] - }, - "scripts": { - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "npm run build:proto && npm run build:proto-types && npm run build:types", - "build:types": "aegir build --no-bundle", - "build:proto": "npm run build:proto:rpc && npm run build:proto:topic-descriptor", - "build:proto:rpc": "pbjs -t static-module -w commonjs -r libp2p-pubsub-rpc --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/pubsub/message/rpc.js ./src/pubsub/message/rpc.proto", - "build:proto:topic-descriptor": "pbjs -t static-module -w commonjs -r libp2p-pubsub-topic-descriptor --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/pubsub/message/topic-descriptor.js ./src/pubsub/message/topic-descriptor.proto", - "build:proto-types": "npm run build:proto-types:rpc && npm run build:proto-types:topic-descriptor", - "build:proto-types:rpc": "pbts -o src/pubsub/message/rpc.d.ts src/pubsub/message/rpc.js", - "build:proto-types:topic-descriptor": "pbts -o src/pubsub/message/topic-descriptor.d.ts src/pubsub/message/topic-descriptor.js", - "test": "aegir test", - "test:node": "aegir test --target node", - "test:browser": "aegir test --target browser", - "release": "aegir release -t node -t browser", - "release-minor": "aegir release --type minor -t node -t browser", - "release-major": "aegir release --type major -t node -t browser" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/libp2p/js-interfaces.git" - }, - "keywords": [ - "libp2p", - "interface" - ], - "author": "", - "license": "MIT", - "bugs": { - "url": "https://github.com/libp2p/js-interfaces/issues" - }, - "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interfaces#readme", - "dependencies": { - "abort-controller": "^3.0.0", - "abortable-iterator": "^3.0.0", - "cids": "^1.1.9", - "debug": "^4.3.1", - "err-code": "^3.0.1", - "it-length-prefixed": "^5.0.2", - "it-pipe": "^1.1.0", - "it-pushable": "^1.4.2", - "libp2p-crypto": "^0.19.5", - "multiaddr": "^10.0.0", - "multiformats": "^9.1.2", - "p-queue": "^6.6.2", - "peer-id": "^0.15.0", - "protobufjs": "^6.10.2", - "uint8arrays": "^3.0.0" - }, - "devDependencies": { - "@types/bl": "^5.0.1", - "@types/debug": "^4.1.5", - "aegir": "^35.0.1", - "events": "^3.3.0", - "it-pair": "^1.0.0", - "p-wait-for": "^3.2.0", - "rimraf": "^3.0.2", - "sinon": "^11.1.1", - "util": "^0.12.3" - }, - "contributors": [ - "Alan Shaw ", - "David Dias ", - "David Dias ", - "Dmitriy Ryajov ", - "Friedel Ziegelmayer ", - "Greg Zuro ", - "Jacob Heun ", - "Jacob Heun ", - "James Ray <16969914+jamesray1@users.noreply.github.com>", - "Jeffrey Hulten ", - "João Santos ", - "Maciej Krüger ", - "Matt Joiner ", - "Mike Goelzer ", - "Patrik Wallstrom ", - "Pau Ramon Revilla ", - "Richard Littauer ", - "Sathya Narrayanan ", - "Vasco Santos ", - "Vasco Santos ", - "dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>", - "dirkmc ", - "dmitriy ryajov ", - "greenkeeperio-bot ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ " - ] -} diff --git a/packages/interfaces/src/connection/connection.js b/packages/interfaces/src/connection/connection.js deleted file mode 100644 index 728a6650f..000000000 --- a/packages/interfaces/src/connection/connection.js +++ /dev/null @@ -1,288 +0,0 @@ -'use strict' - -const PeerId = require('peer-id') -const { Multiaddr } = require('multiaddr') -const errCode = require('err-code') -const { OPEN, CLOSING, CLOSED } = require('./status') - -const connectionSymbol = Symbol.for('@libp2p/interface-connection/connection') - -/** - * @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream - * @typedef {import('./status').Status} Status - */ - -/** - * @typedef {Object} Timeline - * @property {number} open - connection opening timestamp. - * @property {number} [upgraded] - connection upgraded timestamp. - * @property {number} [close] - * - * @typedef {Object} ConectionStat - * @property {'inbound' | 'outbound'} direction - connection establishment direction - * @property {Timeline} timeline - connection relevant events timestamp. - * @property {string} [multiplexer] - connection multiplexing identifier. - * @property {string} [encryption] - connection encryption method identifier. - * - * @typedef {(protocols: string|string[]) => Promise<{stream: MuxedStream, protocol: string}>} CreatedMuxedStream - * - * @typedef {Object} ConnectionOptions - * @property {Multiaddr} [localAddr] - local multiaddr of the connection if known. - * @property {Multiaddr} remoteAddr - remote multiaddr of the connection. - * @property {PeerId} localPeer - local peer-id. - * @property {PeerId} remotePeer - remote peer-id. - * @property {CreatedMuxedStream} newStream - new stream muxer function. - * @property {() => Promise} close - close raw connection function. - * @property {() => MuxedStream[]} getStreams - get streams from muxer function. - * @property {ConectionStat} stat - metadata of the connection. - * - * @typedef {Object} StreamData - * @property {string} protocol - the protocol used by the stream - * @property {Object} [metadata] - metadata of the stream - */ - -/** - * An implementation of the js-libp2p connection. - * Any libp2p transport should use an upgrader to return this connection. - */ -class Connection { - /** - * An implementation of the js-libp2p connection. - * Any libp2p transport should use an upgrader to return this connection. - * - * @class - * @param {ConnectionOptions} options - */ - constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) { - validateArgs(localAddr, localPeer, remotePeer, newStream, close, getStreams, stat) - - /** - * Connection identifier. - */ - this.id = (parseInt(String(Math.random() * 1e9))).toString(36) + Date.now() - - /** - * Observed multiaddr of the local peer - */ - this.localAddr = localAddr - - /** - * Observed multiaddr of the remote peer - */ - this.remoteAddr = remoteAddr - - /** - * Local peer id. - */ - this.localPeer = localPeer - - /** - * Remote peer id. - */ - this.remotePeer = remotePeer - - /** - * Connection metadata. - * - * @type {ConectionStat & {status: Status}} - */ - this._stat = { - ...stat, - status: OPEN - } - - /** - * Reference to the new stream function of the multiplexer - */ - this._newStream = newStream - - /** - * Reference to the close function of the raw connection - */ - this._close = close - - /** - * Reference to the getStreams function of the muxer - */ - this._getStreams = getStreams - - /** - * Connection streams registry - */ - this.registry = new Map() - - /** - * User provided tags - * - * @type {string[]} - */ - this.tags = [] - } - - get [Symbol.toStringTag] () { - return 'Connection' - } - - get [connectionSymbol] () { - return true - } - - /** - * Checks if the given value is a `Connection` instance. - * - * @param {any} other - * @returns {other is Connection} - */ - static isConnection (other) { - return Boolean(other && other[connectionSymbol]) - } - - /** - * Get connection metadata - * - * @this {Connection} - */ - get stat () { - return this._stat - } - - /** - * Get all the streams of the muxer. - * - * @this {Connection} - */ - get streams () { - return this._getStreams() - } - - /** - * Create a new stream from this connection - * - * @param {string|string[]} protocols - intended protocol for the stream - * @returns {Promise<{stream: MuxedStream, protocol: string}>} with muxed+multistream-selected stream and selected protocol - */ - async newStream (protocols) { - if (this.stat.status === CLOSING) { - throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED') - } - - if (this.stat.status === CLOSED) { - throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED') - } - - if (!Array.isArray(protocols)) protocols = [protocols] - - const { stream, protocol } = await this._newStream(protocols) - - this.addStream(stream, { protocol }) - - return { - stream, - protocol - } - } - - /** - * Add a stream when it is opened to the registry. - * - * @param {MuxedStream} muxedStream - a muxed stream - * @param {StreamData} data - the stream data to be registered - * @returns {void} - */ - addStream (muxedStream, { protocol, metadata = {} }) { - // Add metadata for the stream - this.registry.set(muxedStream.id, { - protocol, - ...metadata - }) - } - - /** - * Remove stream registry after it is closed. - * - * @param {string} id - identifier of the stream - */ - removeStream (id) { - this.registry.delete(id) - } - - /** - * Close the connection. - * - * @returns {Promise} - */ - async close () { - if (this.stat.status === CLOSED) { - return - } - - if (this._closing) { - return this._closing - } - - this.stat.status = CLOSING - - // Close raw connection - this._closing = await this._close() - - this._stat.timeline.close = Date.now() - this.stat.status = CLOSED - } -} - -module.exports = Connection - -/** - * @param {Multiaddr|undefined} localAddr - * @param {PeerId} localPeer - * @param {PeerId} remotePeer - * @param {(protocols: string | string[]) => Promise<{ stream: import("../stream-muxer/types").MuxedStream; protocol: string; }>} newStream - * @param {() => Promise} close - * @param {() => import("../stream-muxer/types").MuxedStream[]} getStreams - * @param {{ direction: any; timeline: any; multiplexer?: string | undefined; encryption?: string | undefined; }} stat - */ -function validateArgs (localAddr, localPeer, remotePeer, newStream, close, getStreams, stat) { - if (localAddr && !Multiaddr.isMultiaddr(localAddr)) { - throw errCode(new Error('localAddr must be an instance of multiaddr'), 'ERR_INVALID_PARAMETERS') - } - - if (!PeerId.isPeerId(localPeer)) { - throw errCode(new Error('localPeer must be an instance of peer-id'), 'ERR_INVALID_PARAMETERS') - } - - if (!PeerId.isPeerId(remotePeer)) { - throw errCode(new Error('remotePeer must be an instance of peer-id'), 'ERR_INVALID_PARAMETERS') - } - - if (typeof newStream !== 'function') { - throw errCode(new Error('new stream must be a function'), 'ERR_INVALID_PARAMETERS') - } - - if (typeof close !== 'function') { - throw errCode(new Error('close must be a function'), 'ERR_INVALID_PARAMETERS') - } - - if (typeof getStreams !== 'function') { - throw errCode(new Error('getStreams must be a function'), 'ERR_INVALID_PARAMETERS') - } - - if (!stat) { - throw errCode(new Error('connection metadata object must be provided'), 'ERR_INVALID_PARAMETERS') - } - - if (stat.direction !== 'inbound' && stat.direction !== 'outbound') { - throw errCode(new Error('direction must be "inbound" or "outbound"'), 'ERR_INVALID_PARAMETERS') - } - - if (!stat.timeline) { - throw errCode(new Error('connection timeline object must be provided in the stat object'), 'ERR_INVALID_PARAMETERS') - } - - if (!stat.timeline.open) { - throw errCode(new Error('connection open timestamp must be provided'), 'ERR_INVALID_PARAMETERS') - } - - if (!stat.timeline.upgraded) { - throw errCode(new Error('connection upgraded timestamp must be provided'), 'ERR_INVALID_PARAMETERS') - } -} diff --git a/packages/interfaces/src/connection/index.js b/packages/interfaces/src/connection/index.js deleted file mode 100644 index c4c79fd69..000000000 --- a/packages/interfaces/src/connection/index.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict' - -exports.Connection = require('./connection') diff --git a/packages/interfaces/src/connection/status.js b/packages/interfaces/src/connection/status.js deleted file mode 100644 index 72a8705cd..000000000 --- a/packages/interfaces/src/connection/status.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -const STATUS = { - OPEN: /** @type {'open'} */('open'), - CLOSING: /** @type {'closing'} */('closing'), - CLOSED: /** @type {'closed'} */('closed') -} -module.exports = STATUS - -/** - * @typedef {STATUS[keyof STATUS]} Status - */ diff --git a/packages/interfaces/src/content-routing/types.d.ts b/packages/interfaces/src/content-routing/types.d.ts deleted file mode 100644 index 519587a6d..000000000 --- a/packages/interfaces/src/content-routing/types.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import PeerId from 'peer-id' -import { Multiaddr } from 'multiaddr' -import { CID } from 'multiformats/cid' - -export interface ContentRoutingFactory { - new (options?: any): ContentRouting; -} - -export interface ContentRouting { - provide (cid: CID): Promise; - findProviders (cid: CID, options: Object): AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>; -} - -export default ContentRouting; diff --git a/packages/interfaces/src/crypto/types.d.ts b/packages/interfaces/src/crypto/types.d.ts deleted file mode 100644 index 0815d0d04..000000000 --- a/packages/interfaces/src/crypto/types.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PeerId } from '../peer-id/types' -import { MultiaddrConnection } from '../transport/types' - -/** - * A libp2p crypto module must be compliant to this interface - * to ensure all exchanged data between two peers is encrypted. - */ -export interface Crypto { - protocol: string; - /** - * Encrypt outgoing data to the remote party. - */ - secureOutbound(localPeer: PeerId, connection: MultiaddrConnection, remotePeer: PeerId): Promise; - /** - * Decrypt incoming data. - */ - secureInbound(localPeer: PeerId, connection: MultiaddrConnection, remotePeer?: PeerId): Promise; -} - -export type SecureOutbound = { - conn: MultiaddrConnection; - remoteEarlyData: Buffer; - remotePeer: PeerId; -} diff --git a/packages/interfaces/src/index.js b/packages/interfaces/src/index.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interfaces/src/peer-discovery/types.d.ts b/packages/interfaces/src/peer-discovery/types.d.ts deleted file mode 100644 index b5bfc9420..000000000 --- a/packages/interfaces/src/peer-discovery/types.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EventEmitter } from 'events'; - -export interface PeerDiscoveryFactory { - new (options?: any): PeerDiscovery; - tag: string; -} - -export interface PeerDiscovery extends EventEmitter { - start(): void|Promise; - stop(): void|Promise; -} - -export default PeerDiscovery; diff --git a/packages/interfaces/src/peer-routing/types.d.ts b/packages/interfaces/src/peer-routing/types.d.ts deleted file mode 100644 index 96b448f28..000000000 --- a/packages/interfaces/src/peer-routing/types.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import PeerId from 'peer-id' -import { Multiaddr } from 'multiaddr' - -export interface PeerRoutingFactory { - new (options?: any): PeerRouting; -} - -export interface PeerRouting { - findPeer (peerId: PeerId, options?: Object): Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>; - getClosestPeers(key: Uint8Array, options?: Object): AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>; -} - -export default PeerRouting; diff --git a/packages/interfaces/src/pubsub/message/rpc.d.ts b/packages/interfaces/src/pubsub/message/rpc.d.ts deleted file mode 100644 index 957cc9076..000000000 --- a/packages/interfaces/src/pubsub/message/rpc.d.ts +++ /dev/null @@ -1,243 +0,0 @@ -import * as $protobuf from "protobufjs"; -/** Properties of a RPC. */ -export interface IRPC { - - /** RPC subscriptions */ - subscriptions?: (RPC.ISubOpts[]|null); - - /** RPC msgs */ - msgs?: (RPC.IMessage[]|null); -} - -/** Represents a RPC. */ -export class RPC implements IRPC { - - /** - * Constructs a new RPC. - * @param [p] Properties to set - */ - constructor(p?: IRPC); - - /** RPC subscriptions. */ - public subscriptions: RPC.ISubOpts[]; - - /** RPC msgs. */ - public msgs: RPC.IMessage[]; - - /** - * Encodes the specified RPC message. Does not implicitly {@link RPC.verify|verify} messages. - * @param m RPC message or plain object to encode - * @param [w] Writer to encode to - * @returns Writer - */ - public static encode(m: IRPC, w?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a RPC message from the specified reader or buffer. - * @param r Reader or buffer to decode from - * @param [l] Message length if known beforehand - * @returns RPC - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): RPC; - - /** - * Creates a RPC message from a plain object. Also converts values to their respective internal types. - * @param d Plain object - * @returns RPC - */ - public static fromObject(d: { [k: string]: any }): RPC; - - /** - * Creates a plain object from a RPC message. Also converts values to other types if specified. - * @param m RPC - * @param [o] Conversion options - * @returns Plain object - */ - public static toObject(m: RPC, o?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this RPC to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -} - -export namespace RPC { - - /** Properties of a SubOpts. */ - interface ISubOpts { - - /** SubOpts subscribe */ - subscribe?: (boolean|null); - - /** SubOpts topicID */ - topicID?: (string|null); - } - - /** Represents a SubOpts. */ - class SubOpts implements ISubOpts { - - /** - * Constructs a new SubOpts. - * @param [p] Properties to set - */ - constructor(p?: RPC.ISubOpts); - - /** SubOpts subscribe. */ - public subscribe?: (boolean|null); - - /** SubOpts topicID. */ - public topicID?: (string|null); - - /** SubOpts _subscribe. */ - public _subscribe?: "subscribe"; - - /** SubOpts _topicID. */ - public _topicID?: "topicID"; - - /** - * Encodes the specified SubOpts message. Does not implicitly {@link RPC.SubOpts.verify|verify} messages. - * @param m SubOpts message or plain object to encode - * @param [w] Writer to encode to - * @returns Writer - */ - public static encode(m: RPC.ISubOpts, w?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a SubOpts message from the specified reader or buffer. - * @param r Reader or buffer to decode from - * @param [l] Message length if known beforehand - * @returns SubOpts - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): RPC.SubOpts; - - /** - * Creates a SubOpts message from a plain object. Also converts values to their respective internal types. - * @param d Plain object - * @returns SubOpts - */ - public static fromObject(d: { [k: string]: any }): RPC.SubOpts; - - /** - * Creates a plain object from a SubOpts message. Also converts values to other types if specified. - * @param m SubOpts - * @param [o] Conversion options - * @returns Plain object - */ - public static toObject(m: RPC.SubOpts, o?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this SubOpts to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - } - - /** Properties of a Message. */ - interface IMessage { - - /** Message from */ - from?: (Uint8Array|null); - - /** Message data */ - data?: (Uint8Array|null); - - /** Message seqno */ - seqno?: (Uint8Array|null); - - /** Message topicIDs */ - topicIDs?: (string[]|null); - - /** Message signature */ - signature?: (Uint8Array|null); - - /** Message key */ - key?: (Uint8Array|null); - } - - /** Represents a Message. */ - class Message implements IMessage { - - /** - * Constructs a new Message. - * @param [p] Properties to set - */ - constructor(p?: RPC.IMessage); - - /** Message from. */ - public from?: (Uint8Array|null); - - /** Message data. */ - public data?: (Uint8Array|null); - - /** Message seqno. */ - public seqno?: (Uint8Array|null); - - /** Message topicIDs. */ - public topicIDs: string[]; - - /** Message signature. */ - public signature?: (Uint8Array|null); - - /** Message key. */ - public key?: (Uint8Array|null); - - /** Message _from. */ - public _from?: "from"; - - /** Message _data. */ - public _data?: "data"; - - /** Message _seqno. */ - public _seqno?: "seqno"; - - /** Message _signature. */ - public _signature?: "signature"; - - /** Message _key. */ - public _key?: "key"; - - /** - * Encodes the specified Message message. Does not implicitly {@link RPC.Message.verify|verify} messages. - * @param m Message message or plain object to encode - * @param [w] Writer to encode to - * @returns Writer - */ - public static encode(m: RPC.IMessage, w?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a Message message from the specified reader or buffer. - * @param r Reader or buffer to decode from - * @param [l] Message length if known beforehand - * @returns Message - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): RPC.Message; - - /** - * Creates a Message message from a plain object. Also converts values to their respective internal types. - * @param d Plain object - * @returns Message - */ - public static fromObject(d: { [k: string]: any }): RPC.Message; - - /** - * Creates a plain object from a Message message. Also converts values to other types if specified. - * @param m Message - * @param [o] Conversion options - * @returns Plain object - */ - public static toObject(m: RPC.Message, o?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this Message to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - } -} diff --git a/packages/interfaces/src/pubsub/message/topic-descriptor.d.ts b/packages/interfaces/src/pubsub/message/topic-descriptor.d.ts deleted file mode 100644 index b4dc5cf84..000000000 --- a/packages/interfaces/src/pubsub/message/topic-descriptor.d.ts +++ /dev/null @@ -1,239 +0,0 @@ -import * as $protobuf from "protobufjs"; -/** Properties of a TopicDescriptor. */ -export interface ITopicDescriptor { - - /** TopicDescriptor name */ - name?: (string|null); - - /** TopicDescriptor auth */ - auth?: (TopicDescriptor.IAuthOpts|null); - - /** TopicDescriptor enc */ - enc?: (TopicDescriptor.IEncOpts|null); -} - -/** Represents a TopicDescriptor. */ -export class TopicDescriptor implements ITopicDescriptor { - - /** - * Constructs a new TopicDescriptor. - * @param [p] Properties to set - */ - constructor(p?: ITopicDescriptor); - - /** TopicDescriptor name. */ - public name?: (string|null); - - /** TopicDescriptor auth. */ - public auth?: (TopicDescriptor.IAuthOpts|null); - - /** TopicDescriptor enc. */ - public enc?: (TopicDescriptor.IEncOpts|null); - - /** TopicDescriptor _name. */ - public _name?: "name"; - - /** TopicDescriptor _auth. */ - public _auth?: "auth"; - - /** TopicDescriptor _enc. */ - public _enc?: "enc"; - - /** - * Encodes the specified TopicDescriptor message. Does not implicitly {@link TopicDescriptor.verify|verify} messages. - * @param m TopicDescriptor message or plain object to encode - * @param [w] Writer to encode to - * @returns Writer - */ - public static encode(m: ITopicDescriptor, w?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a TopicDescriptor message from the specified reader or buffer. - * @param r Reader or buffer to decode from - * @param [l] Message length if known beforehand - * @returns TopicDescriptor - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor; - - /** - * Creates a TopicDescriptor message from a plain object. Also converts values to their respective internal types. - * @param d Plain object - * @returns TopicDescriptor - */ - public static fromObject(d: { [k: string]: any }): TopicDescriptor; - - /** - * Creates a plain object from a TopicDescriptor message. Also converts values to other types if specified. - * @param m TopicDescriptor - * @param [o] Conversion options - * @returns Plain object - */ - public static toObject(m: TopicDescriptor, o?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this TopicDescriptor to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -} - -export namespace TopicDescriptor { - - /** Properties of an AuthOpts. */ - interface IAuthOpts { - - /** AuthOpts mode */ - mode?: (TopicDescriptor.AuthOpts.AuthMode|null); - - /** AuthOpts keys */ - keys?: (Uint8Array[]|null); - } - - /** Represents an AuthOpts. */ - class AuthOpts implements IAuthOpts { - - /** - * Constructs a new AuthOpts. - * @param [p] Properties to set - */ - constructor(p?: TopicDescriptor.IAuthOpts); - - /** AuthOpts mode. */ - public mode?: (TopicDescriptor.AuthOpts.AuthMode|null); - - /** AuthOpts keys. */ - public keys: Uint8Array[]; - - /** AuthOpts _mode. */ - public _mode?: "mode"; - - /** - * Encodes the specified AuthOpts message. Does not implicitly {@link TopicDescriptor.AuthOpts.verify|verify} messages. - * @param m AuthOpts message or plain object to encode - * @param [w] Writer to encode to - * @returns Writer - */ - public static encode(m: TopicDescriptor.IAuthOpts, w?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes an AuthOpts message from the specified reader or buffer. - * @param r Reader or buffer to decode from - * @param [l] Message length if known beforehand - * @returns AuthOpts - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor.AuthOpts; - - /** - * Creates an AuthOpts message from a plain object. Also converts values to their respective internal types. - * @param d Plain object - * @returns AuthOpts - */ - public static fromObject(d: { [k: string]: any }): TopicDescriptor.AuthOpts; - - /** - * Creates a plain object from an AuthOpts message. Also converts values to other types if specified. - * @param m AuthOpts - * @param [o] Conversion options - * @returns Plain object - */ - public static toObject(m: TopicDescriptor.AuthOpts, o?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this AuthOpts to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - } - - namespace AuthOpts { - - /** AuthMode enum. */ - enum AuthMode { - NONE = 0, - KEY = 1, - WOT = 2 - } - } - - /** Properties of an EncOpts. */ - interface IEncOpts { - - /** EncOpts mode */ - mode?: (TopicDescriptor.EncOpts.EncMode|null); - - /** EncOpts keyHashes */ - keyHashes?: (Uint8Array[]|null); - } - - /** Represents an EncOpts. */ - class EncOpts implements IEncOpts { - - /** - * Constructs a new EncOpts. - * @param [p] Properties to set - */ - constructor(p?: TopicDescriptor.IEncOpts); - - /** EncOpts mode. */ - public mode?: (TopicDescriptor.EncOpts.EncMode|null); - - /** EncOpts keyHashes. */ - public keyHashes: Uint8Array[]; - - /** EncOpts _mode. */ - public _mode?: "mode"; - - /** - * Encodes the specified EncOpts message. Does not implicitly {@link TopicDescriptor.EncOpts.verify|verify} messages. - * @param m EncOpts message or plain object to encode - * @param [w] Writer to encode to - * @returns Writer - */ - public static encode(m: TopicDescriptor.IEncOpts, w?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes an EncOpts message from the specified reader or buffer. - * @param r Reader or buffer to decode from - * @param [l] Message length if known beforehand - * @returns EncOpts - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor.EncOpts; - - /** - * Creates an EncOpts message from a plain object. Also converts values to their respective internal types. - * @param d Plain object - * @returns EncOpts - */ - public static fromObject(d: { [k: string]: any }): TopicDescriptor.EncOpts; - - /** - * Creates a plain object from an EncOpts message. Also converts values to other types if specified. - * @param m EncOpts - * @param [o] Conversion options - * @returns Plain object - */ - public static toObject(m: TopicDescriptor.EncOpts, o?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this EncOpts to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; - } - - namespace EncOpts { - - /** EncMode enum. */ - enum EncMode { - NONE = 0, - SHAREDKEY = 1, - WOT = 2 - } - } -} diff --git a/packages/interfaces/src/pubsub/peer-streams.js b/packages/interfaces/src/pubsub/peer-streams.js deleted file mode 100644 index d603699a7..000000000 --- a/packages/interfaces/src/pubsub/peer-streams.js +++ /dev/null @@ -1,201 +0,0 @@ -'use strict' - -const debug = require('debug') -const log = Object.assign(debug('libp2p-pubsub:peer-streams'), { - error: debug('libp2p-pubsub:peer-streams:err') -}) -const { EventEmitter } = require('events') - -const lp = require('it-length-prefixed') -const pushable = require('it-pushable') -const { pipe } = require('it-pipe') -const { source: abortable } = require('abortable-iterator') -const AbortController = require('abort-controller').default - -/** - * @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream - * @typedef {import('peer-id')} PeerId - * @typedef {import('it-pushable').Pushable} PushableStream - */ - -/** - * Thin wrapper around a peer's inbound / outbound pubsub streams - */ -class PeerStreams extends EventEmitter { - /** - * @param {object} properties - properties of the PeerStreams. - * @param {PeerId} properties.id - * @param {string} properties.protocol - */ - constructor ({ id, protocol }) { - super() - - /** - * @type {import('peer-id')} - */ - this.id = id - /** - * Established protocol - * - * @type {string} - */ - this.protocol = protocol - /** - * The raw outbound stream, as retrieved from conn.newStream - * - * @private - * @type {null|MuxedStream} - */ - this._rawOutboundStream = null - /** - * The raw inbound stream, as retrieved from the callback from libp2p.handle - * - * @private - * @type {null|MuxedStream} - */ - this._rawInboundStream = null - /** - * An AbortController for controlled shutdown of the inbound stream - * - * @private - * @type {AbortController} - */ - this._inboundAbortController = new AbortController() - /** - * Write stream -- its preferable to use the write method - * - * @type {null|PushableStream} - */ - this.outboundStream = null - /** - * Read stream - * - * @type {null| AsyncIterable} - */ - this.inboundStream = null - } - - /** - * Do we have a connection to read from? - * - * @type {boolean} - */ - get isReadable () { - return Boolean(this.inboundStream) - } - - /** - * Do we have a connection to write on? - * - * @type {boolean} - */ - get isWritable () { - return Boolean(this.outboundStream) - } - - /** - * Send a message to this peer. - * Throws if there is no `stream` to write to available. - * - * @param {Uint8Array} data - * @returns {void} - */ - write (data) { - if (!this.outboundStream) { - const id = this.id.toB58String() - throw new Error('No writable connection to ' + id) - } - - this.outboundStream.push(data) - } - - /** - * Attach a raw inbound stream and setup a read stream - * - * @param {MuxedStream} stream - * @returns {AsyncIterable} - */ - attachInboundStream (stream) { - // Create and attach a new inbound stream - // The inbound stream is: - // - abortable, set to only return on abort, rather than throw - // - transformed with length-prefix transform - this._rawInboundStream = stream - this.inboundStream = abortable( - pipe( - this._rawInboundStream, - lp.decode() - ), - this._inboundAbortController.signal, - { returnOnAbort: true } - ) - - this.emit('stream:inbound') - return this.inboundStream - } - - /** - * Attach a raw outbound stream and setup a write stream - * - * @param {MuxedStream} stream - * @returns {Promise} - */ - async attachOutboundStream (stream) { - // If an outbound stream already exists, gently close it - const _prevStream = this.outboundStream - if (this.outboundStream) { - // End the stream without emitting a close event - await this.outboundStream.end() - } - - this._rawOutboundStream = stream - this.outboundStream = pushable({ - onEnd: (shouldEmit) => { - // close writable side of the stream - this._rawOutboundStream && this._rawOutboundStream.reset && this._rawOutboundStream.reset() - this._rawOutboundStream = null - this.outboundStream = null - if (shouldEmit) { - this.emit('close') - } - } - }) - - pipe( - this.outboundStream, - lp.encode(), - this._rawOutboundStream - ).catch(/** @param {Error} err */ err => { - log.error(err) - }) - - // Only emit if the connection is new - if (!_prevStream) { - this.emit('stream:outbound') - } - } - - /** - * Closes the open connection to peer - * - * @returns {void} - */ - close () { - // End the outbound stream - if (this.outboundStream) { - this.outboundStream.end() - } - // End the inbound stream - if (this.inboundStream) { - this._inboundAbortController.abort() - } - - this._rawOutboundStream = null - this.outboundStream = null - this._rawInboundStream = null - this.inboundStream = null - this.emit('close') - } -} - -module.exports = PeerStreams diff --git a/packages/interfaces/src/pubsub/signature-policy.js b/packages/interfaces/src/pubsub/signature-policy.js deleted file mode 100644 index 7bfb19323..000000000 --- a/packages/interfaces/src/pubsub/signature-policy.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -/** - * Enum for Signature Policy - * Details how message signatures are produced/consumed - */ -const SignaturePolicy = { - /** - * On the producing side: - * * Build messages with the signature, key (from may be enough for certain inlineable public key types), from and seqno fields. - * - * On the consuming side: - * * Enforce the fields to be present, reject otherwise. - * * Propagate only if the fields are valid and signature can be verified, reject otherwise. - */ - StrictSign: /** @type {'StrictSign'} */ ('StrictSign'), - /** - * On the producing side: - * * Build messages without the signature, key, from and seqno fields. - * * The corresponding protobuf key-value pairs are absent from the marshalled message, not just empty. - * - * On the consuming side: - * * Enforce the fields to be absent, reject otherwise. - * * Propagate only if the fields are absent, reject otherwise. - * * A message_id function will not be able to use the above fields, and should instead rely on the data field. A commonplace strategy is to calculate a hash. - */ - StrictNoSign: /** @type {'StrictNoSign'} */ ('StrictNoSign') -} -exports.SignaturePolicy = SignaturePolicy - -/** - * @typedef {SignaturePolicy[keyof SignaturePolicy]} SignaturePolicyType - */ diff --git a/packages/interfaces/src/pubsub/utils.js b/packages/interfaces/src/pubsub/utils.js deleted file mode 100644 index 1005fcf16..000000000 --- a/packages/interfaces/src/pubsub/utils.js +++ /dev/null @@ -1,153 +0,0 @@ -'use strict' - -// @ts-ignore libp2p crypto has no types -const randomBytes = require('libp2p-crypto/src/random-bytes') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const PeerId = require('peer-id') -const { sha256 } = require('multiformats/hashes/sha2') - -/** - * @typedef {import('./message/rpc').RPC.IMessage} IMessage - * @typedef {import('./message/rpc').RPC.Message} Message - * @typedef {import('.').InMessage} NormalizedIMessage - */ - -/** - * Generatea random sequence number. - * - * @returns {Uint8Array} - * @private - */ -const randomSeqno = () => { - return randomBytes(8) -} - -/** - * Generate a message id, based on the `from` and `seqno`. - * - * @param {Uint8Array|string} from - * @param {Uint8Array} seqno - * @returns {Uint8Array} - * @private - */ -const msgId = (from, seqno) => { - let fromBytes - - if (from instanceof Uint8Array) { - fromBytes = PeerId.createFromBytes(from).id - } else { - fromBytes = PeerId.parse(from).id - } - - const msgId = new Uint8Array(fromBytes.length + seqno.length) - msgId.set(fromBytes, 0) - msgId.set(seqno, fromBytes.length) - return msgId -} - -/** - * Generate a message id, based on message `data`. - * - * @param {Uint8Array} data - * @private - */ -const noSignMsgId = (data) => sha256.encode(data) - -/** - * Check if any member of the first set is also a member - * of the second set. - * - * @param {Set|Array} a - * @param {Set|Array} b - * @returns {boolean} - * @private - */ -const anyMatch = (a, b) => { - let bHas - if (Array.isArray(b)) { - /** - * @param {number} val - */ - bHas = (val) => b.indexOf(val) > -1 - } else { - /** - * @param {number} val - */ - bHas = (val) => b.has(val) - } - - for (const val of a) { - if (bHas(val)) { - return true - } - } - - return false -} - -/** - * Make everything an array. - * - * @template T - * @param {T|T[]} maybeArray - * @returns {T[]} - * @private - */ -const ensureArray = (maybeArray) => { - if (!Array.isArray(maybeArray)) { - return [maybeArray] - } - - return maybeArray -} - -/** - * Ensures `message.from` is base58 encoded - * - * @template {{from?:any}} T - * @param {T & IMessage} message - * @param {string} [peerId] - * @returns {NormalizedIMessage} - */ -const normalizeInRpcMessage = (message, peerId) => { - /** @type {NormalizedIMessage} */ - // @ts-ignore receivedFrom not yet defined - const m = Object.assign({}, message) - if (message.from instanceof Uint8Array) { - m.from = uint8ArrayToString(message.from, 'base58btc') - } - if (peerId) { - m.receivedFrom = peerId - } - return m -} - -/** - * @template {{from?:any, data?:any}} T - * - * @param {T & NormalizedIMessage} message - * @returns {Message} - */ -const normalizeOutRpcMessage = (message) => { - /** @type {Message} */ - // @ts-ignore from not yet defined - const m = Object.assign({}, message) - if (typeof message.from === 'string') { - m.from = uint8ArrayFromString(message.from, 'base58btc') - } - if (typeof message.data === 'string') { - m.data = uint8ArrayFromString(message.data) - } - return m -} - -module.exports = { - randomSeqno, - msgId, - noSignMsgId, - anyMatch, - ensureArray, - normalizeInRpcMessage, - normalizeOutRpcMessage -} diff --git a/packages/interfaces/src/stream-muxer/types.d.ts b/packages/interfaces/src/stream-muxer/types.d.ts deleted file mode 100644 index cf0c6f43a..000000000 --- a/packages/interfaces/src/stream-muxer/types.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -import BufferList from 'bl' - -export interface MuxerFactory { - new (options: MuxerOptions): Muxer; - multicodec: string; -} - -/** - * A libp2p stream muxer - */ -export interface Muxer { - readonly streams: Array; - /** - * Initiate a new stream with the given name. If no name is - * provided, the id of th stream will be used. - */ - newStream (name?: string): MuxedStream; - - /** - * A function called when receiving a new stream from the remote. - */ - onStream (stream: MuxedStream): void; - - /** - * A function called when a stream ends. - */ - onStreamEnd (stream: MuxedStream): void; -} - -export type MuxerOptions = { - onStream: (stream: MuxedStream) => void; - onStreamEnd: (stream: MuxedStream) => void; - maxMsgSize?: number; -} - -export type MuxedTimeline = { - open: number; - close?: number; -} - -export interface MuxedStream extends AsyncIterable { - close: () => void; - abort: () => void; - reset: () => void; - sink: Sink; - source: AsyncIterable; - timeline: MuxedTimeline; - id: string; -} - -export type Sink = (source: Uint8Array) => Promise; diff --git a/packages/interfaces/src/topology/index.js b/packages/interfaces/src/topology/index.js deleted file mode 100644 index 85e72db7d..000000000 --- a/packages/interfaces/src/topology/index.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict' - -const noop = () => {} -const topologySymbol = Symbol.for('@libp2p/js-interfaces/topology') - -/** - * @typedef {import('peer-id')} PeerId - */ - -/** - * @typedef {Object} Options - * @property {number} [min=0] - minimum needed connections. - * @property {number} [max=Infinity] - maximum needed connections. - * @property {Handlers} [handlers] - * - * @typedef {Object} Handlers - * @property {(peerId: PeerId, conn: Connection) => void} [onConnect] - protocol "onConnect" handler - * @property {(peerId: PeerId, error?:Error) => void} [onDisconnect] - protocol "onDisconnect" handler - * - * @typedef {import('../connection/connection')} Connection - */ - -class Topology { - /** - * @param {Options} options - */ - constructor ({ - min = 0, - max = Infinity, - handlers = {} - }) { - this.min = min - this.max = max - - // Handlers - this._onConnect = handlers.onConnect || noop - this._onDisconnect = handlers.onDisconnect || noop - - /** - * Set of peers that support the protocol. - * - * @type {Set} - */ - this.peers = new Set() - } - - get [Symbol.toStringTag] () { - return 'Topology' - } - - get [topologySymbol] () { - return true - } - - /** - * Checks if the given value is a Topology instance. - * - * @param {any} other - * @returns {other is Topology} - */ - static isTopology (other) { - return Boolean(other && other[topologySymbol]) - } - - /** - * @param {any} registrar - */ - set registrar (registrar) { // eslint-disable-line - this._registrar = registrar - } - - /** - * Notify about peer disconnected event. - * - * @param {PeerId} peerId - * @returns {void} - */ - disconnect (peerId) { - this._onDisconnect(peerId) - } -} - -module.exports = Topology diff --git a/packages/interfaces/src/topology/multicodec-topology.js b/packages/interfaces/src/topology/multicodec-topology.js deleted file mode 100644 index 02e739ea7..000000000 --- a/packages/interfaces/src/topology/multicodec-topology.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict' - -const Topology = require('./index') -const multicodecTopologySymbol = Symbol.for('@libp2p/js-interfaces/topology/multicodec-topology') - -class MulticodecTopology extends Topology { - /** - * @param {TopologyOptions & MulticodecOptions} props - */ - constructor ({ - min, - max, - multicodecs, - handlers - }) { - super({ min, max, handlers }) - - if (!multicodecs) { - throw new Error('one or more multicodec should be provided') - } - - if (!handlers) { - throw new Error('the handlers should be provided') - } - - if (typeof handlers.onConnect !== 'function') { - throw new Error('the \'onConnect\' handler must be provided') - } - - if (typeof handlers.onDisconnect !== 'function') { - throw new Error('the \'onDisconnect\' handler must be provided') - } - - this.multicodecs = Array.isArray(multicodecs) ? multicodecs : [multicodecs] - this._registrar = undefined - - this._onProtocolChange = this._onProtocolChange.bind(this) - this._onPeerConnect = this._onPeerConnect.bind(this) - } - - get [Symbol.toStringTag] () { - return 'Topology' - } - - get [multicodecTopologySymbol] () { - return true - } - - /** - * Checks if the given value is a `MulticodecTopology` instance. - * - * @param {any} other - * @returns {other is MulticodecTopology} - */ - static isMulticodecTopology (other) { - return Boolean(other && other[multicodecTopologySymbol]) - } - - /** - * @param {any} registrar - */ - set registrar (registrar) { // eslint-disable-line - this._registrar = registrar - this._registrar.peerStore.on('change:protocols', this._onProtocolChange) - this._registrar.connectionManager.on('peer:connect', this._onPeerConnect) - - // Update topology peers - this._updatePeers(this._registrar.peerStore.peers.values()) - } - - /** - * Update topology. - * - * @param {Array<{id: PeerId, multiaddrs: Array, protocols: Array}>} peerDataIterable - * @returns {void} - */ - _updatePeers (peerDataIterable) { - for (const { id, protocols } of peerDataIterable) { - if (this.multicodecs.filter(multicodec => protocols.includes(multicodec)).length) { - // Add the peer regardless of whether or not there is currently a connection - this.peers.add(id.toB58String()) - // If there is a connection, call _onConnect - const connection = this._registrar.getConnection(id) - connection && this._onConnect(id, connection) - } else { - // Remove any peers we might be tracking that are no longer of value to us - this.peers.delete(id.toB58String()) - } - } - } - - /** - * Check if a new peer support the multicodecs for this topology. - * - * @param {Object} props - * @param {PeerId} props.peerId - * @param {Array} props.protocols - */ - _onProtocolChange ({ peerId, protocols }) { - const hadPeer = this.peers.has(peerId.toB58String()) - const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol)) - - // Not supporting the protocol anymore? - if (hadPeer && hasProtocol.length === 0) { - this._onDisconnect(peerId) - } - - // New to protocol support - for (const protocol of protocols) { - if (this.multicodecs.includes(protocol)) { - const peerData = this._registrar.peerStore.get(peerId) - this._updatePeers([peerData]) - return - } - } - } - - /** - * Verify if a new connected peer has a topology multicodec and call _onConnect. - * - * @param {Connection} connection - * @returns {void} - */ - _onPeerConnect (connection) { - // @ts-ignore - remotePeer does not existist on Connection - const peerId = connection.remotePeer - const protocols = this._registrar.peerStore.protoBook.get(peerId) - - if (!protocols) { - return - } - - if (this.multicodecs.find(multicodec => protocols.includes(multicodec))) { - this.peers.add(peerId.toB58String()) - this._onConnect(peerId, connection) - } - } -} - -/** - * @typedef {import('peer-id')} PeerId - * @typedef {import('multiaddr')} Multiaddr - * @typedef {import('../connection/connection')} Connection - * @typedef {import('.').Options} TopologyOptions - * @typedef {Object} MulticodecOptions - * @property {string[]} multicodecs - protocol multicodecs - * @property {Required} handlers - * @typedef {import('.').Handlers} Handlers - */ -module.exports = MulticodecTopology diff --git a/packages/interfaces/src/transport/types.d.ts b/packages/interfaces/src/transport/types.d.ts deleted file mode 100644 index d946ea1fb..000000000 --- a/packages/interfaces/src/transport/types.d.ts +++ /dev/null @@ -1,72 +0,0 @@ -import BufferList from 'bl' -import events from 'events' -import { Multiaddr } from 'multiaddr' -import Connection from '../connection/connection' -import { Sink } from '../stream-muxer/types' - -export interface TransportFactory { - new(upgrader: Upgrader): Transport; -} - -/** - * A libp2p transport is understood as something that offers a dial and listen interface to establish connections. - */ -export interface Transport { - /** - * Dial a given multiaddr. - */ - dial(ma: Multiaddr, options?: DialOptions): Promise; - /** - * Create transport listeners. - */ - createListener(options: ListenerOptions, handler?: (connection: Connection) => void): Listener; - /** - * Takes a list of `Multiaddr`s and returns only valid addresses for the transport - */ - filter(multiaddrs: Multiaddr[]): Multiaddr[]; -} - -export interface Listener extends events.EventEmitter { - /** - * Start a listener - */ - listen(multiaddr: Multiaddr): Promise; - /** - * Get listen addresses - */ - getAddrs(): Multiaddr[]; - /** - * Close listener - * - * @returns {Promise} - */ - close(): Promise; -} - -export interface Upgrader { - /** - * Upgrades an outbound connection on `transport.dial`. - */ - upgradeOutbound(maConn: MultiaddrConnection): Promise; - - /** - * Upgrades an inbound connection on transport listener. - */ - upgradeInbound(maConn: MultiaddrConnection): Promise; -} - -export type MultiaddrConnectionTimeline = { - open: number; - upgraded?: number; - close?: number; -} - -export type MultiaddrConnection = { - sink: Sink; - source: AsyncIterable; - close: (err?: Error) => Promise; - conn: unknown; - remoteAddr: Multiaddr; - localAddr?: Multiaddr; - timeline: MultiaddrConnectionTimeline; -} diff --git a/packages/interfaces/src/types.d.ts b/packages/interfaces/src/types.d.ts deleted file mode 100644 index f84df7a75..000000000 --- a/packages/interfaces/src/types.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type SelectFn = (key: Uint8Array, records: Uint8Array[]) => number -export type ValidateFn = (a: Uint8Array, b: Uint8Array) => Promise - -export type DhtSelectors = { [key: string]: SelectFn } -export type DhtValidators = { [key: string]: { func: ValidateFn } } diff --git a/packages/interfaces/src/value-store/types.d.ts b/packages/interfaces/src/value-store/types.d.ts deleted file mode 100644 index ed89ac6a1..000000000 --- a/packages/interfaces/src/value-store/types.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { PeerId } from '../peer-id/types' - -export interface GetValueResult { - from: PeerId, - val: Uint8Array, -} - -export interface ValueStore { - put (key: Uint8Array, value: Uint8Array, options?: Object): Promise - get (key: Uint8Array, options?: Object): Promise -} - -export default ValueStore diff --git a/packages/interfaces/test/connection/index.spec.js b/packages/interfaces/test/connection/index.spec.js deleted file mode 100644 index a774eb02d..000000000 --- a/packages/interfaces/test/connection/index.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { Connection } = require('../../src/connection') -const peers = require('../utils/peers') -const PeerId = require('peer-id') -const pair = require('it-pair') - -describe('connection tests', () => { - it('should not require local or remote addrs', async () => { - const [localPeer, remotePeer] = await Promise.all([ - PeerId.createFromJSON(peers[0]), - PeerId.createFromJSON(peers[1]) - ]) - const openStreams = [] - let streamId = 0 - - return new Connection({ - localPeer, - remotePeer, - stat: { - timeline: { - open: Date.now() - 10, - upgraded: Date.now() - }, - direction: 'outbound', - encryption: '/secio/1.0.0', - multiplexer: '/mplex/6.7.0' - }, - newStream: (protocols) => { - const id = streamId++ - const stream = pair() - - stream.close = () => stream.sink([]) - stream.id = id - - openStreams.push(stream) - - return { - stream, - protocol: protocols[0] - } - }, - close: () => {}, - getStreams: () => openStreams - }) - }) -}) diff --git a/packages/interfaces/test/pubsub/emit-self.spec.js b/packages/interfaces/test/pubsub/emit-self.spec.js deleted file mode 100644 index 9743452c9..000000000 --- a/packages/interfaces/test/pubsub/emit-self.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') - -const { - createPeerId, - mockRegistrar, - PubsubImplementation -} = require('./utils') - -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const protocol = '/pubsub/1.0.0' -const topic = 'foo' -const data = uint8ArrayFromString('bar') -const shouldNotHappen = (_) => expect.fail() - -describe('emitSelf', () => { - let pubsub - - describe('enabled', () => { - before(async () => { - const peerId = await createPeerId() - - pubsub = new PubsubImplementation(protocol, { - peerId, - registrar: mockRegistrar - }, { emitSelf: true }) - }) - - before(() => { - pubsub.start() - pubsub.subscribe(topic) - }) - - after(() => { - pubsub.stop() - }) - - it('should emit to self on publish', () => { - const promise = new Promise((resolve) => pubsub.once(topic, resolve)) - - pubsub.publish(topic, data) - - return promise - }) - }) - - describe('disabled', () => { - before(async () => { - const peerId = await createPeerId() - - pubsub = new PubsubImplementation(protocol, { - peerId, - registrar: mockRegistrar - }, { emitSelf: false }) - }) - - before(() => { - pubsub.start() - pubsub.subscribe(topic) - }) - - after(() => { - pubsub.stop() - }) - - it('should not emit to self on publish', () => { - pubsub.once(topic, (m) => shouldNotHappen) - - pubsub.publish(topic, data) - - // Wait 1 second to guarantee that self is not noticed - return new Promise((resolve) => setTimeout(() => resolve(), 1000)) - }) - }) -}) diff --git a/packages/interfaces/test/pubsub/instance.spec.js b/packages/interfaces/test/pubsub/instance.spec.js deleted file mode 100644 index 7ec3bc662..000000000 --- a/packages/interfaces/test/pubsub/instance.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') - -const PubsubBaseImpl = require('../../src/pubsub') -const { - createPeerId, - mockRegistrar -} = require('./utils') - -describe('pubsub instance', () => { - let peerId - - before(async () => { - peerId = await createPeerId() - }) - - it('should throw if no debugName is provided', () => { - expect(() => { - new PubsubBaseImpl() // eslint-disable-line no-new - }).to.throw() - }) - - it('should throw if no multicodec is provided', () => { - expect(() => { - new PubsubBaseImpl({ // eslint-disable-line no-new - debugName: 'pubsub' - }) - }).to.throw() - }) - - it('should throw if no libp2p is provided', () => { - expect(() => { - new PubsubBaseImpl({ // eslint-disable-line no-new - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0' - }) - }).to.throw() - }) - - it('should accept valid parameters', () => { - expect(() => { - new PubsubBaseImpl({ // eslint-disable-line no-new - debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', - libp2p: { - peerId: peerId, - registrar: mockRegistrar - } - }) - }).not.to.throw() - }) -}) diff --git a/packages/interfaces/test/pubsub/utils.spec.js b/packages/interfaces/test/pubsub/utils.spec.js deleted file mode 100644 index 50d1cf0d0..000000000 --- a/packages/interfaces/test/pubsub/utils.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const utils = require('../../src/pubsub/utils') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -describe('utils', () => { - it('randomSeqno', () => { - const first = utils.randomSeqno() - const second = utils.randomSeqno() - - expect(first).to.have.length(8) - expect(second).to.have.length(8) - expect(first).to.not.eql(second) - }) - - it('msgId should not generate same ID for two different Uint8Arrays', () => { - const peerId = 'QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22' - const msgId0 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfde', 'base16')) - const msgId1 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfe0', 'base16')) - expect(msgId0).to.not.deep.equal(msgId1) - }) - - it('anyMatch', () => { - [ - [[1, 2, 3], [4, 5, 6], false], - [[1, 2], [1, 2], true], - [[1, 2, 3], [4, 5, 1], true], - [[5, 6, 1], [1, 2, 3], true], - [[], [], false], - [[1], [2], false] - ].forEach((test) => { - expect(utils.anyMatch(new Set(test[0]), new Set(test[1]))) - .to.eql(test[2]) - - expect(utils.anyMatch(new Set(test[0]), test[1])) - .to.eql(test[2]) - }) - }) - - it('ensureArray', () => { - expect(utils.ensureArray('hello')).to.be.eql(['hello']) - expect(utils.ensureArray([1, 2])).to.be.eql([1, 2]) - }) - - it('converts an IN msg.from to b58', () => { - const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') - const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' - const m = [ - { from: binaryId }, - { from: stringId } - ] - const expected = [ - { from: stringId }, - { from: stringId } - ] - for (let i = 0; i < m.length; i++) { - expect(utils.normalizeInRpcMessage(m[i])).to.deep.eql(expected[i]) - } - }) - - it('converts an OUT msg.from to binary', () => { - const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') - const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' - const m = [ - { from: binaryId }, - { from: stringId } - ] - const expected = [ - { from: binaryId }, - { from: binaryId } - ] - for (let i = 0; i < m.length; i++) { - expect(utils.normalizeOutRpcMessage(m[i])).to.deep.eql(expected[i]) - } - }) -}) diff --git a/packages/interfaces/test/pubsub/utils/index.js b/packages/interfaces/test/pubsub/utils/index.js deleted file mode 100644 index 4d037c488..000000000 --- a/packages/interfaces/test/pubsub/utils/index.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict' - -const DuplexPair = require('it-pair/duplex') - -const PeerId = require('peer-id') - -const PubsubBaseProtocol = require('../../../src/pubsub') -const { RPC } = require('../../../src/pubsub/message/rpc') - -exports.createPeerId = async () => { - const peerId = await PeerId.create({ bits: 1024 }) - - return peerId -} - -class PubsubImplementation extends PubsubBaseProtocol { - constructor (protocol, libp2p, options = {}) { - super({ - debugName: 'libp2p:pubsub', - multicodecs: protocol, - libp2p, - ...options - }) - } - - _publish (message) { - // ... - } - - _decodeRpc (bytes) { - return RPC.decode(bytes) - } - - _encodeRpc (rpc) { - return RPC.encode(rpc).finish() - } -} - -exports.PubsubImplementation = PubsubImplementation - -exports.mockRegistrar = { - handle: () => {}, - register: () => {}, - unregister: () => {} -} - -exports.createMockRegistrar = (registrarRecord) => ({ - handle: (multicodecs, handler) => { - const rec = registrarRecord[multicodecs[0]] || {} - - registrarRecord[multicodecs[0]] = { - ...rec, - handler - } - }, - register: ({ multicodecs, _onConnect, _onDisconnect }) => { - const rec = registrarRecord[multicodecs[0]] || {} - - registrarRecord[multicodecs[0]] = { - ...rec, - onConnect: _onConnect, - onDisconnect: _onDisconnect - } - - return multicodecs[0] - }, - unregister: (id) => { - delete registrarRecord[id] - } -}) - -exports.ConnectionPair = () => { - const [d0, d1] = DuplexPair() - - return [ - { - stream: d0, - newStream: () => Promise.resolve({ stream: d0 }) - }, - { - stream: d1, - newStream: () => Promise.resolve({ stream: d1 }) - } - ] -} diff --git a/packages/interfaces/tsconfig.json b/packages/interfaces/tsconfig.json deleted file mode 100644 index 1865dfc28..000000000 --- a/packages/interfaces/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src" - ], - "exclude": [ - "src/pubsub/message/rpc.js", // exclude generated file - "src/pubsub/message/topic-descriptor.js" // exclude generated file - ] -} diff --git a/packages/libp2p-connection/package.json b/packages/libp2p-connection/package.json new file mode 100644 index 000000000..85c3e8399 --- /dev/null +++ b/packages/libp2p-connection/package.json @@ -0,0 +1,64 @@ +{ + "name": "libp2p-connection", + "version": "0.0.1", + "description": "JS Libp2p connections", + "type": "module", + "files": [ + "src", + "dist" + ], + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "*/index", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "eslintConfig": { + "extends": "ipfs" + }, + "scripts": { + "lint": "aegir lint", + "dep-check": "aegir dep-check dist/src/**/*.js", + "pretest": "npm run build", + "build": "tsc", + "test": "aegir test -f ./dist/test/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-interfaces.git" + }, + "keywords": [ + "libp2p", + "interface" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-interfaces/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-connection#readme#readme", + "dependencies": { + "err-code": "^3.0.1", + "libp2p-interfaces": "^1.2.0", + "multiaddr": "^10.0.1", + "peer-id": "^0.15.3" + }, + "devDependencies": { + "aegir": "^36.0.0" + }, + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "./status": { + "import": "./dist/src/status.js", + "types": "./dist/src/status.d.ts" + } + } +} diff --git a/packages/libp2p-connection/src/index.ts b/packages/libp2p-connection/src/index.ts new file mode 100644 index 000000000..f71da7494 --- /dev/null +++ b/packages/libp2p-connection/src/index.ts @@ -0,0 +1,182 @@ +import type { Multiaddr } from 'multiaddr' +import errCode from 'err-code' +import { OPEN, CLOSING, CLOSED } from 'libp2p-interfaces/connection/status' +import type { MuxedStream } from 'libp2p-interfaces/stream-muxer' +import type { ConnectionStat, StreamData } from 'libp2p-interfaces/connection' +import type { PeerId } from 'libp2p-interfaces/peer-id' + +const connectionSymbol = Symbol.for('@libp2p/interface-connection/connection') + +export interface ProtocolStream { + protocol: string + stream: MuxedStream +} + +interface ConnectionOptions { + localAddr: Multiaddr + remoteAddr: Multiaddr + localPeer: PeerId + remotePeer: PeerId + newStream: (protocols: string[]) => Promise + close: () => Promise + getStreams: () => MuxedStream[] + stat: ConnectionStat +} + +/** + * An implementation of the js-libp2p connection. + * Any libp2p transport should use an upgrader to return this connection. + */ +export class Connection { + /** + * Connection identifier. + */ + public readonly id: string + /** + * Observed multiaddr of the local peer + */ + public readonly localAddr: Multiaddr + /** + * Observed multiaddr of the remote peer + */ + public readonly remoteAddr: Multiaddr + /** + * Local peer id + */ + public readonly localPeer: PeerId + /** + * Remote peer id + */ + public readonly remotePeer: PeerId + /** + * Connection metadata + */ + public readonly stat: ConnectionStat + /** + * User provided tags + * + */ + public tags: string[] + + /** + * Reference to the new stream function of the multiplexer + */ + private readonly _newStream: (protocols: string[]) => Promise + /** + * Reference to the close function of the raw connection + */ + private readonly _close: () => Promise + /** + * Reference to the getStreams function of the muxer + */ + private readonly _getStreams: () => MuxedStream[] + /** + * Connection streams registry + */ + public readonly registry: Map + private _closing: boolean + + /** + * An implementation of the js-libp2p connection. + * Any libp2p transport should use an upgrader to return this connection. + */ + constructor (options: ConnectionOptions) { + const { localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat } = options + + this.id = `${(parseInt(String(Math.random() * 1e9))).toString(36)}${Date.now()}` + this.localAddr = localAddr + this.remoteAddr = remoteAddr + this.localPeer = localPeer + this.remotePeer = remotePeer + this.stat = { + ...stat, + status: OPEN + } + this._newStream = newStream + this._close = close + this._getStreams = getStreams + this.registry = new Map() + this.tags = [] + this._closing = false + } + + get [Symbol.toStringTag] () { + return 'Connection' + } + + get [connectionSymbol] () { + return true + } + + /** + * Checks if the given value is a `Connection` instance + */ + static isConnection (other: any) { + return Boolean(connectionSymbol in other) + } + + /** + * Get all the streams of the muxer + */ + get streams () { + return this._getStreams() + } + + /** + * Create a new stream from this connection + */ + async newStream (protocols: string[]) { + if (this.stat.status === CLOSING) { + throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED') + } + + if (this.stat.status === CLOSED) { + throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED') + } + + if (!Array.isArray(protocols)) protocols = [protocols] + + const { stream, protocol } = await this._newStream(protocols) + + this.addStream(stream, { protocol, metadata: {} }) + + return { + stream, + protocol + } + } + + /** + * Add a stream when it is opened to the registry + */ + addStream (muxedStream: MuxedStream, streamData: StreamData) { + // Add metadata for the stream + this.registry.set(muxedStream.id, streamData) + } + + /** + * Remove stream registry after it is closed + */ + removeStream (id: string) { + this.registry.delete(id) + } + + /** + * Close the connection + */ + async close () { + if (this.stat.status === CLOSED || this._closing) { + return + } + + this.stat.status = CLOSING + + // Close raw connection + this._closing = true + await this._close() + this._closing = false + + this.stat.timeline.close = Date.now() + this.stat.status = CLOSED + } +} diff --git a/packages/interfaces/test/utils/peers.js b/packages/libp2p-connection/test/index.spec.ts similarity index 88% rename from packages/interfaces/test/utils/peers.js rename to packages/libp2p-connection/test/index.spec.ts index fad0d23e8..b5e9e8e5f 100644 --- a/packages/interfaces/test/utils/peers.js +++ b/packages/libp2p-connection/test/index.spec.ts @@ -1,6 +1,11 @@ -'use strict' +import { Connection } from '../src/index.js' +import PeerIdFactory from 'peer-id' +// @ts-expect-error no types +import pair from 'it-pair' +import { Multiaddr } from 'multiaddr' +import type { MuxedStream } from 'libp2p-interfaces/stream-muxer' -module.exports = [{ +const peers = [{ id: 'QmNMMAqSxPetRS1cVMmutW5BCN1qQQyEr4u98kUvZjcfEw', privKey: 'CAASpQkwggShAgEAAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAECggEAB2H2uPRoRCAKU+T3gO4QeoiJaYKNjIO7UCplE0aMEeHDnEjAKC1HQ1G0DRdzZ8sb0fxuIGlNpFMZv5iZ2ZFg2zFfV//DaAwTek9tIOpQOAYHUtgHxkj5FIlg2BjlflGb+ZY3J2XsVB+2HNHkUEXOeKn2wpTxcoJE07NmywkO8Zfr1OL5oPxOPlRN1gI4ffYH2LbfaQVtRhwONR2+fs5ISfubk5iKso6BX4moMYkxubYwZbpucvKKi/rIjUA3SK86wdCUnno1KbDfdXSgCiUlvxt/IbRFXFURQoTV6BOi3sP5crBLw8OiVubMr9/8WE6KzJ0R7hPd5+eeWvYiYnWj4QKBgQD6jRlAFo/MgPO5NZ/HRAk6LUG+fdEWexA+GGV7CwJI61W/Dpbn9ZswPDhRJKo3rquyDFVZPdd7+RlXYg1wpmp1k54z++L1srsgj72vlg4I8wkZ4YLBg0+zVgHlQ0kxnp16DvQdOgiRFvMUUMEgetsoIx1CQWTd67hTExGsW+WAZQKBgQDT/WaHWvwyq9oaZ8G7F/tfeuXvNTk3HIJdfbWGgRXB7lJ7Gf6FsX4x7PeERfL5a67JLV6JdiLLVuYC2CBhipqLqC2DB962aKMvxobQpSljBBZvZyqP1IGPoKskrSo+2mqpYkeCLbDMuJ1nujgMP7gqVjabs2zj6ACKmmpYH/oNowJ/T0ZVtvFsjkg+1VsiMupUARRQuPUWMwa9HOibM1NIZcoQV2NGXB5Z++kR6JqxQO0DZlKArrviclderUdY+UuuY4VRiSEprpPeoW7ZlbTku/Ap8QZpWNEzZorQDro7bnfBW91fX9/81ets/gCPGrfEn+58U3pdb9oleCOQc/ifpQKBgBTYGbi9bYbd9vgZs6bd2M2um+VFanbMytS+g5bSIn2LHXkVOT2UEkB+eGf9KML1n54QY/dIMmukA8HL1oNAyalpw+/aWj+9Ui5kauUhGEywHjSeBEVYM9UXizxz+m9rsoktLLLUI0o97NxCJzitG0Kub3gn0FEogsUeIc7AdinZAoGBANnM1vcteSQDs7x94TDEnvvqwSkA2UWyLidD2jXgE0PG4V6tTkK//QPBmC9eq6TIqXkzYlsErSw4XeKO91knFofmdBzzVh/ddgx/NufJV4tXF+a2iTpqYBUJiz9wpIKgf43/Ob+P1EA99GAhSdxz1ess9O2aTqf3ANzn6v6g62Pv', pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAE=' @@ -25,3 +30,53 @@ module.exports = [{ privKey: 'CAASpwkwggSjAgEAAoIBAQC1/GFud/7xutux7qRfMj1sIdMRh99/chR6HqVj6LQqrgk4jil0mdN/LCk/tqPqmDtObHdmEhCoybzuhLbCKgUqryKDwO6yBJHSKWY9QqrKZtLJ37SgKwGjE3+NUD4r1dJHhtQrICFdOdSCBzs/v8gi+J+KZLHo7+Nms4z09ysy7qZh94Pd7cW4gmSMergqUeANLD9C0ERw1NXolswOW7Bi7UGr7yuBxejICLO3nkxe0OtpQBrYrqdCD9vs3t/HQZbPWVoiRj4VO7fxkAPKLl30HzcIfxj/ayg8NHcH59d08D+N2v5Sdh28gsiYKIPE9CXvuw//HUY2WVRY5fDC5JglAgMBAAECggEBAKb5aN/1w3pBqz/HqRMbQpYLNuD33M3PexBNPAy+P0iFpDo63bh5Rz+A4lvuFNmzUX70MFz7qENlzi6+n/zolxMB29YtWBUH8k904rTEjXXl//NviQgITZk106tx+4k2x5gPEm57LYGfBOdFAUzNhzDnE2LkXwRNzkS161f7zKwOEsaGWRscj6UvhO4MIFxjb32CVwt5eK4yOVqtyMs9u30K4Og+AZYTlhtm+bHg6ndCCBO6CQurCQ3jD6YOkT+L3MotKqt1kORpvzIB0ujZRf49Um8wlcjC5G9aexBeGriXaVdPF62zm7GA7RMsbQM/6aRbA1fEQXvJhHUNF9UFeaECgYEA8wCjKqQA7UQnHjRwTsktdwG6szfxd7z+5MTqHHTWhWzgcQLgdh5/dO/zanEoOThadMk5C1Bqjq96gH2xim8dg5XQofSVtV3Ui0dDa+XRB3E3fyY4D3RF5hHv85O0GcvQc6DIb+Ja1oOhvHowFB1C+CT3yEgwzX/EK9xpe+KtYAkCgYEAv7hCnj/DcZFU3fAfS+unBLuVoVJT/drxv66P686s7J8UM6tW+39yDBZ1IcwY9vHFepBvxY2fFfEeLI02QFM+lZXVhNGzFkP90agNHK01psGgrmIufl9zAo8WOKgkLgbYbSHzkkDeqyjEPU+B0QSsZOCE+qLCHSdsnTmo/TjQhj0CgYAz1+j3yfGgrS+jVBC53lXi0+2fGspbf2jqKdDArXSvFqFzuudki/EpY6AND4NDYfB6hguzjD6PnoSGMUrVfAtR7X6LbwEZpqEX7eZGeMt1yQPMDr1bHrVi9mS5FMQR1NfuM1lP9Xzn00GIUpE7WVrWUhzDEBPJY/7YVLf0hFH08QKBgDWBRQZJIVBmkNrHktRrVddaSq4U/d/Q5LrsCrpymYwH8WliHgpeTQPWmKXwAd+ZJdXIzYjCt202N4eTeVqGYOb6Q/anV2WVYBbM4avpIxoA28kPGY6nML+8EyWIt2ApBOmgGgvtEreNzwaVU9NzjHEyv6n7FlVwlT1jxCe3XWq5AoGASYPKQoPeDlW+NmRG7z9EJXJRPVtmLL40fmGgtju9QIjLnjuK8XaczjAWT+ySI93Whu+Eujf2Uj7Q+NfUjvAEzJgwzuOd3jlQvoALq11kuaxlNQTn7rx0A1QhBgUJE8AkvShPC9FEnA4j/CLJU0re9H/8VvyN6qE0Mho0+YbjpP8=', pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1/GFud/7xutux7qRfMj1sIdMRh99/chR6HqVj6LQqrgk4jil0mdN/LCk/tqPqmDtObHdmEhCoybzuhLbCKgUqryKDwO6yBJHSKWY9QqrKZtLJ37SgKwGjE3+NUD4r1dJHhtQrICFdOdSCBzs/v8gi+J+KZLHo7+Nms4z09ysy7qZh94Pd7cW4gmSMergqUeANLD9C0ERw1NXolswOW7Bi7UGr7yuBxejICLO3nkxe0OtpQBrYrqdCD9vs3t/HQZbPWVoiRj4VO7fxkAPKLl30HzcIfxj/ayg8NHcH59d08D+N2v5Sdh28gsiYKIPE9CXvuw//HUY2WVRY5fDC5JglAgMBAAE=' }] + +describe('connection tests', () => { + it('should not require local or remote addrs', async () => { + const localPeer = await PeerIdFactory.createFromJSON(peers[0]) + const remotePeer = await PeerIdFactory.createFromJSON(peers[1]) + + const openStreams: any[] = [] + let streamId = 0 + + return new Connection({ + localPeer, + localAddr: new Multiaddr('/ip4/127.0.0.1/tcp/4001'), + remotePeer, + remoteAddr: new Multiaddr('/ip4/127.0.0.1/tcp/4002'), + stat: { + timeline: { + open: Date.now() - 10, + upgraded: Date.now() + }, + direction: 'outbound', + encryption: '/secio/1.0.0', + multiplexer: '/mplex/6.7.0', + status: 'OPEN' + }, + newStream: async (protocols) => { + const id = `${streamId++}` + const stream: MuxedStream = { + ...pair(), + close: async () => await stream.sink([]), + id, + abort: () => {}, + reset: () => {}, + timeline: { + open: 0 + }, + [Symbol.asyncIterator]: () => stream.source + } + + openStreams.push(stream) + + return { + stream, + protocol: protocols[0] + } + }, + close: async () => {}, + getStreams: () => openStreams + }) + }) +}) diff --git a/packages/libp2p-connection/tsconfig.json b/packages/libp2p-connection/tsconfig.json new file mode 100644 index 000000000..4b68f4ffa --- /dev/null +++ b/packages/libp2p-connection/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../libp2p-interfaces" + } + ] +} diff --git a/packages/compliance-tests/CHANGELOG.md b/packages/libp2p-interfaces-compliance-tests/CHANGELOG.md similarity index 100% rename from packages/compliance-tests/CHANGELOG.md rename to packages/libp2p-interfaces-compliance-tests/CHANGELOG.md diff --git a/packages/compliance-tests/README.md b/packages/libp2p-interfaces-compliance-tests/README.md similarity index 100% rename from packages/compliance-tests/README.md rename to packages/libp2p-interfaces-compliance-tests/README.md diff --git a/packages/libp2p-interfaces-compliance-tests/package.json b/packages/libp2p-interfaces-compliance-tests/package.json new file mode 100644 index 000000000..ccb8842d7 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/package.json @@ -0,0 +1,72 @@ +{ + "name": "libp2p-interfaces-compliance-tests", + "version": "1.1.2", + "description": "Compliance tests for JS libp2p interfaces", + "type": "module", + "files": [ + "src", + "dist" + ], + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "eslintConfig": { + "extends": "ipfs" + }, + "scripts": { + "lint": "aegir lint", + "dep-check": "aegir dep-check dist/src/**/*.js", + "build": "tsc", + "pretest": "npm run build", + "test": "aegir test -f ./dist/test/**/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-interfaces.git" + }, + "keywords": [ + "libp2p", + "interface" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-interfaces/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces-compliance-tests#readme#readme", + "dependencies": { + "abort-controller": "^3.0.0", + "abortable-iterator": "^3.0.0", + "aegir": "^36.0.0", + "chai": "^4.3.4", + "chai-checkmark": "^1.0.1", + "delay": "^5.0.0", + "it-goodbye": "^3.0.0", + "it-pair": "^1.0.0", + "it-pipe": "^1.1.0", + "libp2p-connection": "^0.0.1", + "libp2p-crypto": "^0.19.5", + "libp2p-interfaces": "^1.2.0", + "libp2p-pubsub": "^0.6.0", + "libp2p-topology": "^0.0.1", + "multiaddr": "^10.0.0", + "multiformats": "^9.4.10", + "p-defer": "^3.0.0", + "p-limit": "^3.1.0", + "p-wait-for": "^4.1.0", + "peer-id": "^0.15.0", + "sinon": "^11.1.1", + "streaming-iterables": "^6.0.0", + "uint8arrays": "^3.0.0", + "util": "^0.12.4" + }, + "devDependencies": { + "it-handshake": "^2.0.0" + } +} diff --git a/packages/interfaces/src/connection/README.md b/packages/libp2p-interfaces-compliance-tests/src/connection/README.md similarity index 99% rename from packages/interfaces/src/connection/README.md rename to packages/libp2p-interfaces-compliance-tests/src/connection/README.md index 8b0b411f4..db1b3da59 100644 --- a/packages/interfaces/src/connection/README.md +++ b/packages/libp2p-interfaces-compliance-tests/src/connection/README.md @@ -25,7 +25,7 @@ This helps ensuring that the transport is responsible for socket management, whi ### Test suite ```js -const tests = require('libp2p-interfaces-compliance-tests/src/connection') +const tests = require('libp2p-interfaces-compliance-tests/connection') describe('your connection', () => { tests({ // Options should be passed to your connection diff --git a/packages/compliance-tests/src/connection/connection.js b/packages/libp2p-interfaces-compliance-tests/src/connection/connection.ts similarity index 81% rename from packages/compliance-tests/src/connection/connection.js rename to packages/libp2p-interfaces-compliance-tests/src/connection/connection.ts index 5a8dbc484..2830fc05b 100644 --- a/packages/compliance-tests/src/connection/connection.js +++ b/packages/libp2p-interfaces-compliance-tests/src/connection/connection.ts @@ -1,20 +1,15 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import type { TestSetup } from '../index.js' +import type { Connection } from 'libp2p-interfaces/connection' -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') -const Status = require('libp2p-interfaces/src/connection/status') - -module.exports = (test) => { +export default (test: TestSetup) => { describe('connection', () => { describe('open connection', () => { - let connection + let connection: Connection beforeEach(async () => { connection = await test.setup() - if (!connection) throw new Error('missing connection') }) afterEach(async () => { @@ -28,7 +23,7 @@ module.exports = (test) => { expect(connection.remotePeer).to.exist() expect(connection.localAddr).to.exist() expect(connection.remoteAddr).to.exist() - expect(connection.stat.status).to.equal(Status.OPEN) + expect(connection.stat.status).to.equal('OPEN') expect(connection.stat.timeline.open).to.exist() expect(connection.stat.timeline.upgraded).to.exist() expect(connection.stat.timeline.close).to.not.exist() @@ -40,7 +35,7 @@ module.exports = (test) => { it('should get the metadata of an open connection', () => { const stat = connection.stat - expect(stat.status).to.equal(Status.OPEN) + expect(stat.status).to.equal('OPEN') expect(stat.direction).to.exist() expect(stat.timeline.open).to.exist() expect(stat.timeline.upgraded).to.exist() @@ -55,7 +50,7 @@ module.exports = (test) => { it('should be able to create a new stream', async () => { const protocolToUse = '/echo/0.0.1' - const { stream, protocol } = await connection.newStream(protocolToUse) + const { stream, protocol } = await connection.newStream([protocolToUse]) expect(protocol).to.equal(protocolToUse) @@ -69,11 +64,11 @@ module.exports = (test) => { }) describe('close connection', () => { - let connection + let connection: Connection let timelineProxy const proxyHandler = { set () { - // @ts-ignore - TS fails to infer here + // @ts-expect-error - TS fails to infer here return Reflect.set(...arguments) } } @@ -92,7 +87,6 @@ module.exports = (test) => { multiplexer: '/muxer/1.0.0' } }) - if (!connection) throw new Error('missing connection') }) afterEach(async () => { @@ -104,26 +98,26 @@ module.exports = (test) => { await connection.close() expect(connection.stat.timeline.close).to.exist() - expect(connection.stat.status).to.equal(Status.CLOSED) + expect(connection.stat.status).to.equal('CLOSED') }) it('should be able to close the connection after opening a stream', async () => { // Open stream const protocol = '/echo/0.0.1' - await connection.newStream(protocol) + await connection.newStream([protocol]) // Close connection expect(connection.stat.timeline.close).to.not.exist() await connection.close() expect(connection.stat.timeline.close).to.exist() - expect(connection.stat.status).to.equal(Status.CLOSED) + expect(connection.stat.status).to.equal('CLOSED') }) it('should properly track streams', async () => { // Open stream const protocol = '/echo/0.0.1' - const { stream } = await connection.newStream(protocol) + const { stream } = await connection.newStream([protocol]) const trackedStream = connection.registry.get(stream.id) expect(trackedStream).to.have.property('protocol', protocol) @@ -138,9 +132,9 @@ module.exports = (test) => { expect(connection.stat.timeline.close).to.not.exist() await connection.close() - // @ts-ignore - fails to infer callCount + // @ts-expect-error - fails to infer callCount expect(proxyHandler.set.callCount).to.equal(1) - // @ts-ignore - fails to infer getCall + // @ts-expect-error - fails to infer getCall const [obj, key, value] = proxyHandler.set.getCall(0).args expect(obj).to.eql(connection.stat.timeline) expect(key).to.equal('close') @@ -149,14 +143,16 @@ module.exports = (test) => { it('should fail to create a new stream if the connection is closing', async () => { expect(connection.stat.timeline.close).to.not.exist() - connection.close() + const p = connection.close() try { const protocol = '/echo/0.0.1' - await connection.newStream(protocol) - } catch (err) { + await connection.newStream([protocol]) + } catch (err: any) { expect(err).to.exist() return + } finally { + await p } throw new Error('should fail to create a new stream if the connection is closing') @@ -168,8 +164,8 @@ module.exports = (test) => { try { const protocol = '/echo/0.0.1' - await connection.newStream(protocol) - } catch (err) { + await connection.newStream([protocol]) + } catch (err: any) { expect(err).to.exist() expect(err.code).to.equal('ERR_CONNECTION_CLOSED') return diff --git a/packages/libp2p-interfaces-compliance-tests/src/connection/index.ts b/packages/libp2p-interfaces-compliance-tests/src/connection/index.ts new file mode 100644 index 000000000..cfeb617e2 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/connection/index.ts @@ -0,0 +1,7 @@ +import connectionSuite from './connection.js' +import type { TestSetup } from '../index.js' +import type { Connection } from 'libp2p-interfaces/connection' + +export default (test: TestSetup) => { + connectionSuite(test) +} diff --git a/packages/compliance-tests/src/crypto/index.js b/packages/libp2p-interfaces-compliance-tests/src/crypto/index.ts similarity index 67% rename from packages/compliance-tests/src/crypto/index.js rename to packages/libp2p-interfaces-compliance-tests/src/crypto/index.ts index cb0d9175f..daa601974 100644 --- a/packages/compliance-tests/src/crypto/index.js +++ b/packages/libp2p-interfaces-compliance-tests/src/crypto/index.ts @@ -1,23 +1,22 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const duplexPair = require('it-pair/duplex') -const { pipe } = require('it-pipe') -const PeerId = require('peer-id') -const { collect } = require('streaming-iterables') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const peers = require('../utils/peers') -const { UnexpectedPeerError } = require('libp2p-interfaces/src/crypto/errors') - -module.exports = (common) => { +import { expect } from 'aegir/utils/chai.js' +// @ts-expect-error no types +import duplexPair from 'it-pair/duplex.js' +import { pipe } from 'it-pipe' +import PeerIdFactory from 'peer-id' +import { collect } from 'streaming-iterables' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import peers from '../utils/peers.js' +import { UnexpectedPeerError } from 'libp2p-interfaces/crypto/errors' +import type { TestSetup } from '../index.js' +import type { Crypto } from 'libp2p-interfaces/crypto' +import type { PeerId } from 'libp2p-interfaces/peer-id' + +export default (common: TestSetup) => { describe('interface-crypto compliance tests', () => { - let crypto - let localPeer - let remotePeer - let mitmPeer + let crypto: Crypto + let localPeer: PeerId + let remotePeer: PeerId + let mitmPeer: PeerId before(async () => { [ @@ -27,13 +26,15 @@ module.exports = (common) => { mitmPeer ] = await Promise.all([ common.setup(), - PeerId.createFromJSON(peers[0]), - PeerId.createFromJSON(peers[1]), - PeerId.createFromJSON(peers[2]) + PeerIdFactory.createFromJSON(peers[0]), + PeerIdFactory.createFromJSON(peers[1]), + PeerIdFactory.createFromJSON(peers[2]) ]) }) - after(() => common.teardown && common.teardown()) + after(async () => { + await common.teardown() + }) it('has a protocol string', () => { expect(crypto.protocol).to.exist() @@ -60,7 +61,7 @@ module.exports = (common) => { [input], outboundResult.conn, // Convert BufferList to Buffer via slice - (source) => (async function * toBuffer () { + (source: AsyncIterable) => (async function * toBuffer () { for await (const chunk of source) { yield chunk.slice() } @@ -94,7 +95,7 @@ module.exports = (common) => { await Promise.all([ crypto.secureInbound(remotePeer, localConn, mitmPeer), crypto.secureOutbound(localPeer, remoteConn, remotePeer) - ]).then(expect.fail, (err) => { + ]).then(() => expect.fail(), (err) => { expect(err).to.exist() expect(err).to.have.property('code', UnexpectedPeerError.code) }) diff --git a/packages/libp2p-interfaces-compliance-tests/src/index.ts b/packages/libp2p-interfaces-compliance-tests/src/index.ts new file mode 100644 index 000000000..aba7dd462 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/index.ts @@ -0,0 +1,5 @@ + +export interface TestSetup { + setup: (args?: SetupArgs) => Promise + teardown: () => Promise +} diff --git a/packages/compliance-tests/src/peer-discovery/index.js b/packages/libp2p-interfaces-compliance-tests/src/peer-discovery/index.ts similarity index 79% rename from packages/compliance-tests/src/peer-discovery/index.js rename to packages/libp2p-interfaces-compliance-tests/src/peer-discovery/index.ts index 4fe175d03..318aa3b21 100644 --- a/packages/compliance-tests/src/peer-discovery/index.js +++ b/packages/libp2p-interfaces-compliance-tests/src/peer-discovery/index.ts @@ -1,17 +1,14 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const PeerId = require('peer-id') - -const delay = require('delay') -const pDefer = require('p-defer') - -module.exports = (common) => { +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from 'multiaddr' +import PeerIdFactory from 'peer-id' +import delay from 'delay' +import pDefer from 'p-defer' +import type { TestSetup } from '../index.js' +import type { PeerDiscovery } from 'libp2p-interfaces/peer-discovery' + +export default (common: TestSetup) => { describe('interface-peer-discovery compliance tests', () => { - let discovery + let discovery: PeerDiscovery beforeEach(async () => { discovery = await common.setup() @@ -22,7 +19,7 @@ module.exports = (common) => { discovery.removeAllListeners() - common.teardown && common.teardown() + await common.teardown() }) it('can start the service', async () => { @@ -49,7 +46,7 @@ module.exports = (common) => { discovery.on('peer', ({ id, multiaddrs }) => { expect(id).to.exist() - expect(PeerId.isPeerId(id)).to.eql(true) + expect(PeerIdFactory.isPeerId(id)).to.eql(true) expect(multiaddrs).to.exist() multiaddrs.forEach((m) => expect(Multiaddr.isMultiaddr(m)).to.eql(true)) diff --git a/packages/compliance-tests/src/peer-id/index.js b/packages/libp2p-interfaces-compliance-tests/src/peer-id/index.ts similarity index 91% rename from packages/compliance-tests/src/peer-id/index.js rename to packages/libp2p-interfaces-compliance-tests/src/peer-id/index.ts index fa20d01ed..b9cbf736a 100644 --- a/packages/compliance-tests/src/peer-id/index.js +++ b/packages/libp2p-interfaces-compliance-tests/src/peer-id/index.ts @@ -1,17 +1,15 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const crypto = require('libp2p-crypto') -const { CID } = require('multiformats/cid') -const Digest = require('multiformats/hashes/digest') -const { base16 } = require('multiformats/bases/base16') -const { base36 } = require('multiformats/bases/base36') -const { base58btc } = require('multiformats/bases/base58') -const { identity } = require('multiformats/hashes/identity') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') +import { expect } from 'aegir/utils/chai.js' +import crypto from 'libp2p-crypto' +import { CID } from 'multiformats/cid' +import Digest from 'multiformats/hashes/digest' +import { base16 } from 'multiformats/bases/base16' +import { base36 } from 'multiformats/bases/base36' +import { base58btc } from 'multiformats/bases/base58' +import { identity } from 'multiformats/hashes/identity' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { TestSetup } from '../index.js' +import type { PeerIdFactory } from 'libp2p-interfaces/peer-id' const DAG_PB_CODE = 0x70 const LIBP2P_KEY_CODE = 0x72 @@ -41,17 +39,16 @@ const testIdB36String = base36.encode(testIdBytes) const testIdCID = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) const testIdCIDString = testIdCID.toString() -module.exports = (common) => { +export default (common: TestSetup) => { describe('interface-peer-id compliance tests', () => { - /** @type {import('libp2p-interfaces/src/peer-id/types').PeerIdFactory} */ - let factory + let factory: PeerIdFactory beforeEach(async () => { factory = await common.setup() }) - afterEach(() => { - common.teardown && common.teardown() + afterEach(async () => { + await common.teardown() }) it('create a new id', async () => { @@ -82,7 +79,7 @@ module.exports = (common) => { const id = await factory.create(testOpts) expect(id.toB58String().length).to.equal(46) expect(() => { - // @ts-ignore + // @ts-expect-error id.id = uint8ArrayFromString('hello') }).to.throw(/immutable/) }) @@ -251,7 +248,12 @@ module.exports = (common) => { it('Pretty printing', async () => { const id1 = await factory.create(testOpts) const json = id1.toJSON() - const id2 = await factory.createFromPrivKey(json.privKey || 'invalid, should not happen') + + if (json.privKey == null) { + throw new Error('No private key found in JSON output') + } + + const id2 = await factory.createFromPrivKey(json.privKey) expect(id1.toPrint()).to.be.eql(id2.toPrint()) expect(id1.toPrint()).to.equal('') }) @@ -261,18 +263,6 @@ module.exports = (common) => { expect(uint8ArrayToString(id.toBytes(), 'base16')).to.equal(uint8ArrayToString(testIdBytes, 'base16')) }) - it('isEqual', async () => { - const ids = await Promise.all([ - factory.create(testOpts), - factory.create(testOpts) - ]) - - expect(ids[0].isEqual(ids[0])).to.equal(true) - expect(ids[0].isEqual(ids[1])).to.equal(false) - expect(ids[0].isEqual(ids[0].id)).to.equal(true) - expect(ids[0].isEqual(ids[1].id)).to.equal(false) - }) - it('equals', async () => { const ids = await Promise.all([ factory.create(testOpts), @@ -325,28 +315,28 @@ module.exports = (common) => { it('set privKey (valid)', async () => { const peerId = await factory.create(testOpts) - // @ts-ignore + // @ts-expect-error peerId.privKey = peerId._privKey expect(peerId.isValid()).to.equal(true) }) it('set pubKey (valid)', async () => { const peerId = await factory.create(testOpts) - // @ts-ignore + // @ts-expect-error peerId.pubKey = peerId._pubKey expect(peerId.isValid()).to.equal(true) }) it('set privKey (invalid)', async () => { const peerId = await factory.create(testOpts) - // @ts-ignore + // @ts-expect-error peerId.privKey = uint8ArrayFromString('bufff') expect(peerId.isValid()).to.equal(false) }) it('set pubKey (invalid)', async () => { const peerId = await factory.create(testOpts) - // @ts-ignore + // @ts-expect-error peerId.pubKey = uint8ArrayFromString('bufff') expect(peerId.isValid()).to.equal(false) }) @@ -364,9 +354,9 @@ module.exports = (common) => { }) describe('throws on inconsistent data', () => { - let k1 - let k2 - let k3 + let k1: crypto.keys.supportedKeys.rsa.RsaPrivateKey + let k2: crypto.keys.supportedKeys.rsa.RsaPrivateKey + let k3: crypto.keys.supportedKeys.rsa.RsaPrivateKey before(async () => { const keys = await Promise.all([ @@ -382,29 +372,29 @@ module.exports = (common) => { it('missmatch private - public key', async () => { const digest = await k1.public.hash() - expect(() => { - factory.createFromJSON({ - id: digest, - pubKey: k1, - privKey: k2.public - }) // eslint-disable-line no-new + expect(async () => { + return await factory.createFromJSON({ + id: uint8ArrayToString(digest, 'base58btc'), + pubKey: uint8ArrayToString(k1.bytes, 'base64pad'), + privKey: uint8ArrayToString(k2.bytes, 'base64pad') + }) }).to.throw(/inconsistent arguments/) }) it('missmatch id - private - public key', async () => { const digest = await k1.public.hash() - expect(() => { - factory.createFromJSON({ - id: digest, - pubKey: k1, - privKey: k3.public + expect(async () => { + return await factory.createFromJSON({ + id: uint8ArrayToString(digest, 'base58btc'), + pubKey: uint8ArrayToString(k1.bytes, 'base64pad'), + privKey: uint8ArrayToString(k3.bytes, 'base64pad') }) // eslint-disable-line no-new }).to.throw(/inconsistent arguments/) }) it('invalid id', () => { // @ts-expect-error incorrect constructor arg type - expect(() => factory.createFromJSON('hello world')).to.throw(/invalid id/) + expect(async () => await factory.createFromJSON('hello world')).to.throw(/invalid id/) }) }) }) diff --git a/packages/compliance-tests/src/pubsub/api.js b/packages/libp2p-interfaces-compliance-tests/src/pubsub/api.ts similarity index 67% rename from packages/compliance-tests/src/pubsub/api.js rename to packages/libp2p-interfaces-compliance-tests/src/pubsub/api.ts index c0b6a33ff..b3f252851 100644 --- a/packages/compliance-tests/src/pubsub/api.js +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/api.ts @@ -1,41 +1,36 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const pDefer = require('p-defer') -const pWaitFor = require('p-wait-for') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import pDefer from 'p-defer' +import pWaitFor from 'p-wait-for' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { TestSetup } from '../index.js' +import type { PubSub } from 'libp2p-interfaces/pubsub' const topic = 'foo' const data = uint8ArrayFromString('bar') -module.exports = (common) => { +export default (common: TestSetup) => { describe('pubsub api', () => { - let pubsub + let pubsub: PubSub // Create pubsub router beforeEach(async () => { - [pubsub] = await common.setup(1) + pubsub = await common.setup() }) afterEach(async () => { sinon.restore() - pubsub && pubsub.stop() + pubsub.stop() await common.teardown() }) it('can start correctly', () => { - sinon.spy(pubsub.registrar, '_handle') sinon.spy(pubsub.registrar, 'register') pubsub.start() expect(pubsub.started).to.eql(true) - expect(pubsub.registrar._handle.callCount).to.eql(1) - expect(pubsub.registrar.register.callCount).to.eql(1) + expect(pubsub.registrar.register).to.have.property('callCount', 1) }) it('can stop correctly', () => { @@ -45,7 +40,7 @@ module.exports = (common) => { pubsub.stop() expect(pubsub.started).to.eql(false) - expect(pubsub.registrar.unregister.callCount).to.eql(1) + expect(pubsub.registrar.unregister).to.have.property('callCount', 1) }) it('can subscribe and unsubscribe correctly', async () => { @@ -64,7 +59,7 @@ module.exports = (common) => { pubsub.unsubscribe(topic) - await pWaitFor(() => !pubsub.getTopics().length) + await pWaitFor(() => pubsub.getTopics().length === 0) // Publish to guarantee the handler is not called await pubsub.publish(topic, data) @@ -75,7 +70,7 @@ module.exports = (common) => { it('can subscribe and publish correctly', async () => { const defer = pDefer() - const handler = (msg) => { + const handler = (msg: Uint8Array) => { expect(msg).to.not.eql(undefined) defer.resolve() } diff --git a/packages/compliance-tests/src/pubsub/connection-handlers.js b/packages/libp2p-interfaces-compliance-tests/src/pubsub/connection-handlers.ts similarity index 77% rename from packages/compliance-tests/src/pubsub/connection-handlers.js rename to packages/libp2p-interfaces-compliance-tests/src/pubsub/connection-handlers.ts index c04489e76..cf631227d 100644 --- a/packages/compliance-tests/src/pubsub/connection-handlers.js +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/connection-handlers.ts @@ -1,24 +1,22 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') -const pDefer = require('p-defer') -const pWaitFor = require('p-wait-for') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const { expectSet } = require('./utils') - -module.exports = (common) => { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import pDefer from 'p-defer' +import pWaitFor from 'p-wait-for' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { expectSet } from './utils.js' +import type { TestSetup } from '../index.js' +import type { PubSub, Message } from 'libp2p-interfaces/pubsub' + +export default (common: TestSetup) => { describe('pubsub connection handlers', () => { - let psA, psB + let psA: PubSub, psB: PubSub describe('nodes send state on connection', () => { // Create pubsub nodes and connect them before(async () => { - [psA, psB] = await common.setup(2) + psA = await common.setup() + psB = await common.setup() expect(psA.peers.size).to.be.eql(0) expect(psB.peers.size).to.be.eql(0) @@ -46,6 +44,7 @@ module.exports = (common) => { it('existing subscriptions are sent upon peer connection', async function () { await Promise.all([ + // @ts-expect-error protected fields psA._libp2p.dial(psB.peerId), new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)), new Promise((resolve) => psB.once('pubsub:subscription-change', resolve)) @@ -55,9 +54,11 @@ module.exports = (common) => { expect(psB.peers.size).to.equal(1) expectSet(psA.subscriptions, ['Za']) + expectSet(psB.topics.get('Za'), [psA.peerId.toB58String()]) expectSet(psB.subscriptions, ['Zb']) + expectSet(psA.topics.get('Zb'), [psB.peerId.toB58String()]) }) }) @@ -65,7 +66,8 @@ module.exports = (common) => { describe('pubsub started before connect', () => { // Create pubsub nodes and start them beforeEach(async () => { - [psA, psB] = await common.setup(2) + psA = await common.setup() + psB = await common.setup() psA.start() psB.start() @@ -78,10 +80,11 @@ module.exports = (common) => { }) it('should get notified of connected peers on dial', async () => { + // @ts-expect-error protected fields const connection = await psA._libp2p.dial(psB.peerId) expect(connection).to.exist() - return Promise.all([ + return await Promise.all([ pWaitFor(() => psA.peers.size === 1), pWaitFor(() => psB.peers.size === 1) ]) @@ -92,6 +95,7 @@ module.exports = (common) => { const topic = 'test-topic' const data = uint8ArrayFromString('hey!') + // @ts-expect-error protected fields await psA._libp2p.dial(psB.peerId) let subscribedTopics = psA.getTopics() @@ -111,7 +115,7 @@ module.exports = (common) => { const subscribedPeers = psB.getSubscribers(topic) return subscribedPeers.includes(psA.peerId.toB58String()) }) - psB.publish(topic, data) + void psB.publish(topic, data) await defer.promise }) @@ -120,19 +124,21 @@ module.exports = (common) => { describe('pubsub started after connect', () => { // Create pubsub nodes beforeEach(async () => { - [psA, psB] = await common.setup(2) + psA = await common.setup() + psB = await common.setup() }) afterEach(async () => { sinon.restore() - psA && psA.stop() - psB && psB.stop() + psA.stop() + psB.stop() await common.teardown() }) it('should get notified of connected peers after starting', async () => { + // @ts-expect-error protected fields const connection = await psA._libp2p.dial(psB.peerId) expect(connection).to.exist() expect(psA.peers.size).to.be.eql(0) @@ -141,7 +147,7 @@ module.exports = (common) => { psA.start() psB.start() - return Promise.all([ + return await Promise.all([ pWaitFor(() => psA.peers.size === 1), pWaitFor(() => psB.peers.size === 1) ]) @@ -152,6 +158,7 @@ module.exports = (common) => { const topic = 'test-topic' const data = uint8ArrayFromString('hey!') + // @ts-expect-error protected fields await psA._libp2p.dial(psB.peerId) psA.start() @@ -179,7 +186,7 @@ module.exports = (common) => { const subscribedPeers = psB.getSubscribers(topic) return subscribedPeers.includes(psA.peerId.toB58String()) }) - psB.publish(topic, data) + void psB.publish(topic, data) await defer.promise }) @@ -188,7 +195,8 @@ module.exports = (common) => { describe('pubsub with intermittent connections', () => { // Create pubsub nodes and start them beforeEach(async () => { - [psA, psB] = await common.setup(2) + psA = await common.setup() + psB = await common.setup() psA.start() psB.start() @@ -197,8 +205,8 @@ module.exports = (common) => { afterEach(async () => { sinon.restore() - psA && psA.stop() - psB && psB.stop() + psA.stop() + psB.stop() await common.teardown() }) @@ -212,6 +220,7 @@ module.exports = (common) => { const defer1 = pDefer() const defer2 = pDefer() + // @ts-expect-error protected fields await psA._libp2p.dial(psB.peerId) let subscribedTopics = psA.getTopics() @@ -232,17 +241,28 @@ module.exports = (common) => { const subscribedPeers = psB.getSubscribers(topic) return subscribedPeers.includes(psAid) }) - psB.publish(topic, data) + void psB.publish(topic, data) await defer1.promise psB.stop() + // @ts-expect-error protected fields await psB._libp2p.stop() - await pWaitFor(() => !psA._libp2p.connectionManager.get(psB.peerId) && !psB._libp2p.connectionManager.get(psA.peerId)) + await pWaitFor(() => { + // @ts-expect-error protected fields + const aHasConnectionToB = psA._libp2p.connectionManager.get(psB.peerId) + // @ts-expect-error protected fields + const bHasConnectionToA = psB._libp2p.connectionManager.get(psA.peerId) + + return aHasConnectionToB != null && bHasConnectionToA != null + }) + // @ts-expect-error protected fields await psB._libp2p.start() psB.start() + // @ts-expect-error protected fields psA._libp2p.peerStore.addressBook.set(psB.peerId, psB._libp2p.multiaddrs) + // @ts-expect-error protected fields await psA._libp2p.dial(psB.peerId) // wait for remoteLibp2p to know about libp2p subscription @@ -251,7 +271,7 @@ module.exports = (common) => { return subscribedPeers.includes(psAid) }) - psB.publish(topic, data) + void psB.publish(topic, data) await defer2.promise }) @@ -263,7 +283,7 @@ module.exports = (common) => { let bReceivedFirstMessageFromA = false let bReceivedSecondMessageFromA = false - const handlerSpyA = (message) => { + const handlerSpyA = (message: Message) => { const data = uint8ArrayToString(message.data) if (data === 'message-from-b-1') { @@ -274,7 +294,7 @@ module.exports = (common) => { aReceivedSecondMessageFromB = true } } - const handlerSpyB = (message) => { + const handlerSpyB = (message: Message) => { const data = uint8ArrayToString(message.data) if (data === 'message-from-a-1') { @@ -294,9 +314,12 @@ module.exports = (common) => { psB.subscribe(topic) // Create two connections to the remote peer + // @ts-expect-error protected fields const originalConnection = await psA._libp2p.dialer.connectToPeer(psB.peerId) // second connection + // @ts-expect-error protected fields await psA._libp2p.dialer.connectToPeer(psB.peerId) + // @ts-expect-error protected fields expect(psA._libp2p.connections.get(psB.peerId.toB58String())).to.have.length(2) // Wait for subscriptions to occur @@ -306,21 +329,22 @@ module.exports = (common) => { }) // Verify messages go both ways - psA.publish(topic, uint8ArrayFromString('message-from-a-1')) - psB.publish(topic, uint8ArrayFromString('message-from-b-1')) + void psA.publish(topic, uint8ArrayFromString('message-from-a-1')) + void psB.publish(topic, uint8ArrayFromString('message-from-b-1')) await pWaitFor(() => { return aReceivedFirstMessageFromB && bReceivedFirstMessageFromA }) // Disconnect the first connection (this acts as a delayed reconnect) + // @ts-expect-error protected fields const psAConnUpdateSpy = sinon.spy(psA._libp2p.connectionManager.connections, 'set') await originalConnection.close() await pWaitFor(() => psAConnUpdateSpy.callCount === 1) // Verify messages go both ways after the disconnect - psA.publish(topic, uint8ArrayFromString('message-from-a-2')) - psB.publish(topic, uint8ArrayFromString('message-from-b-2')) + void psA.publish(topic, uint8ArrayFromString('message-from-a-2')) + void psB.publish(topic, uint8ArrayFromString('message-from-b-2')) await pWaitFor(() => { return aReceivedSecondMessageFromB && bReceivedSecondMessageFromA }) diff --git a/packages/libp2p-interfaces-compliance-tests/src/pubsub/emit-self.ts b/packages/libp2p-interfaces-compliance-tests/src/pubsub/emit-self.ts new file mode 100644 index 000000000..d721a1a59 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/emit-self.ts @@ -0,0 +1,66 @@ +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { TestSetup } from '../index.js' +import type { PubSub, PubsubOptions } from 'libp2p-interfaces/pubsub' + +const topic = 'foo' +const data = uint8ArrayFromString('bar') +const shouldNotHappen = () => expect.fail() + +export default (common: TestSetup>) => { + describe('emit self', () => { + let pubsub: PubSub + + describe('enabled', () => { + before(async () => { + pubsub = await common.setup({ emitSelf: true }) + }) + + before(() => { + pubsub.start() + pubsub.subscribe(topic) + }) + + after(async () => { + sinon.restore() + pubsub.stop() + await common.teardown() + }) + + it('should emit to self on publish', async () => { + const promise = new Promise((resolve) => pubsub.once(topic, resolve)) + + void pubsub.publish(topic, data) + + return await promise + }) + }) + + describe('disabled', () => { + before(async () => { + pubsub = await common.setup({ emitSelf: false }) + }) + + before(() => { + pubsub.start() + pubsub.subscribe(topic) + }) + + after(async () => { + sinon.restore() + pubsub.stop() + await common.teardown() + }) + + it('should not emit to self on publish', async () => { + pubsub.once(topic, () => shouldNotHappen) + + void pubsub.publish(topic, data) + + // Wait 1 second to guarantee that self is not noticed + return await new Promise((resolve) => setTimeout(resolve, 1000)) + }) + }) + }) +} diff --git a/packages/libp2p-interfaces-compliance-tests/src/pubsub/index.ts b/packages/libp2p-interfaces-compliance-tests/src/pubsub/index.ts new file mode 100644 index 000000000..7c81cdfad --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/index.ts @@ -0,0 +1,19 @@ +import apiTest from './api.js' +import emitSelfTest from './emit-self.js' +import messagesTest from './messages.js' +import connectionHandlersTest from './connection-handlers.js' +import twoNodesTest from './two-nodes.js' +import multipleNodesTest from './multiple-nodes.js' +import type { TestSetup } from '../index.js' +import type { PubSub } from 'libp2p-interfaces/pubsub' + +export default (common: TestSetup) => { + describe('interface-pubsub compliance tests', () => { + apiTest(common) + emitSelfTest(common) + messagesTest(common) + connectionHandlersTest(common) + twoNodesTest(common) + multipleNodesTest(common) + }) +} diff --git a/packages/compliance-tests/src/pubsub/messages.js b/packages/libp2p-interfaces-compliance-tests/src/pubsub/messages.ts similarity index 53% rename from packages/compliance-tests/src/pubsub/messages.js rename to packages/libp2p-interfaces-compliance-tests/src/pubsub/messages.ts index 2797563e4..b3db0ddca 100644 --- a/packages/compliance-tests/src/pubsub/messages.js +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/messages.ts @@ -1,43 +1,40 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const PeerId = require('peer-id') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const { utils } = require('libp2p-interfaces/src/pubsub') -const PeerStreams = require('libp2p-interfaces/src/pubsub/peer-streams') -const { SignaturePolicy } = require('libp2p-interfaces/src/pubsub/signature-policy') +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import PeerIdFactory from 'peer-id' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as utils from 'libp2p-pubsub/utils' +import { PeerStreams } from 'libp2p-pubsub/peer-streams' +import type { TestSetup } from '../index.js' +import type { PubSub } from 'libp2p-interfaces/pubsub' const topic = 'foo' const data = uint8ArrayFromString('bar') -module.exports = (common) => { +export default (common: TestSetup) => { describe('messages', () => { - let pubsub + let pubsub: PubSub // Create pubsub router beforeEach(async () => { - [pubsub] = await common.setup(1) + pubsub = await common.setup() pubsub.start() }) afterEach(async () => { sinon.restore() - pubsub && pubsub.stop() + pubsub.stop() await common.teardown() }) it('should emit normalized signed messages on publish', async () => { - pubsub.globalSignaturePolicy = SignaturePolicy.StrictSign + pubsub.globalSignaturePolicy = 'StrictSign' + // @ts-expect-error protected field sinon.spy(pubsub, '_emitMessage') await pubsub.publish(topic, data) + // @ts-expect-error protected field expect(pubsub._emitMessage.callCount).to.eql(1) - + // @ts-expect-error protected field const [messageToEmit] = pubsub._emitMessage.getCall(0).args expect(messageToEmit.seqno).to.not.eql(undefined) @@ -46,12 +43,14 @@ module.exports = (common) => { }) it('should drop unsigned messages', async () => { + // @ts-expect-error protected field sinon.spy(pubsub, '_emitMessage') + // @ts-expect-error protected field sinon.spy(pubsub, '_publish') sinon.spy(pubsub, 'validate') const peerStream = new PeerStreams({ - id: await PeerId.create(), + id: await PeerIdFactory.create(), protocol: 'test' }) const rpc = { @@ -66,21 +65,26 @@ module.exports = (common) => { } pubsub.subscribe(topic) + // @ts-expect-error protected field await pubsub._processRpc(peerStream.id.toB58String(), peerStream, rpc) - expect(pubsub.validate.callCount).to.eql(1) - expect(pubsub._emitMessage.called).to.eql(false) - expect(pubsub._publish.called).to.eql(false) + expect(pubsub.validate).to.have.property('callCount', 1) + // @ts-expect-error protected field + expect(pubsub._emitMessage).to.have.property('called', false) + // @ts-expect-error protected field + expect(pubsub._publish).to.have.property('called', false) }) it('should not drop unsigned messages if strict signing is disabled', async () => { - pubsub.globalSignaturePolicy = SignaturePolicy.StrictNoSign + pubsub.globalSignaturePolicy = 'StrictNoSign' + // @ts-expect-error protected field sinon.spy(pubsub, '_emitMessage') + // @ts-expect-error protected field sinon.spy(pubsub, '_publish') sinon.spy(pubsub, 'validate') const peerStream = new PeerStreams({ - id: await PeerId.create(), + id: await PeerIdFactory.create(), protocol: 'test' }) @@ -93,11 +97,14 @@ module.exports = (common) => { } pubsub.subscribe(topic) + // @ts-expect-error protected field await pubsub._processRpc(peerStream.id.toB58String(), peerStream, rpc) - expect(pubsub.validate.callCount).to.eql(1) - expect(pubsub._emitMessage.called).to.eql(true) - expect(pubsub._publish.called).to.eql(true) + expect(pubsub.validate).to.have.property('callCount', 1) + // @ts-expect-error protected field + expect(pubsub._emitMessage).to.have.property('called', 1) + // @ts-expect-error protected field + expect(pubsub._publish).to.have.property('called', 1) }) }) } diff --git a/packages/compliance-tests/src/pubsub/multiple-nodes.js b/packages/libp2p-interfaces-compliance-tests/src/pubsub/multiple-nodes.ts similarity index 81% rename from packages/compliance-tests/src/pubsub/multiple-nodes.js rename to packages/libp2p-interfaces-compliance-tests/src/pubsub/multiple-nodes.ts index 437e4efcc..ade8c139f 100644 --- a/packages/compliance-tests/src/pubsub/multiple-nodes.js +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/multiple-nodes.ts @@ -1,39 +1,39 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 6] */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const delay = require('delay') -const pDefer = require('p-defer') -const pWaitFor = require('p-wait-for') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const { expectSet } = require('./utils') - -module.exports = (common) => { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import delay from 'delay' +import pDefer from 'p-defer' +import pWaitFor from 'p-wait-for' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { expectSet } from './utils.js' +import type { TestSetup } from '../index.js' +import type { PubSub, Message } from 'libp2p-interfaces/pubsub' + +export default (common: TestSetup) => { describe('pubsub with multiple nodes', function () { describe('every peer subscribes to the topic', () => { describe('line', () => { // line // ◉────◉────◉ // a b c - let psA, psB, psC + let psA: PubSub, psB: PubSub, psC: PubSub // Create and start pubsub nodes beforeEach(async () => { - [psA, psB, psC] = await common.setup(3) + psA = await common.setup() + psB = await common.setup() + psC = await common.setup() - // Start pubsub mpdes + // Start pubsub modes ;[psA, psB, psC].map((p) => p.start()) }) // Connect nodes beforeEach(async () => { + // @ts-expect-error protected field await psA._libp2p.dial(psB.peerId) + // @ts-expect-error protected field await psB._libp2p.dial(psC.peerId) // Wait for peers to be ready in pubsub @@ -84,7 +84,7 @@ module.exports = (common) => { expectSet(psC.topics.get(topic), [psB.peerId.toB58String()]) }) - it('subscribe to the topic on node c', () => { + it('subscribe to the topic on node c', async () => { const topic = 'Z' const defer = pDefer() @@ -99,7 +99,7 @@ module.exports = (common) => { defer.resolve() }) - return defer.promise + return await defer.promise }) it('publish on node a', async () => { @@ -126,9 +126,9 @@ module.exports = (common) => { psB.on(topic, incMsg) psC.on(topic, incMsg) - psA.publish(topic, uint8ArrayFromString('hey')) + void psA.publish(topic, uint8ArrayFromString('hey')) - function incMsg (msg) { + function incMsg (msg: Message) { expect(uint8ArrayToString(msg.data)).to.equal('hey') check() } @@ -142,7 +142,7 @@ module.exports = (common) => { } } - return defer.promise + return await defer.promise }) // since the topology is the same, just the publish @@ -177,9 +177,9 @@ module.exports = (common) => { // await a cycle await delay(1000) - psB.publish(topic, uint8ArrayFromString('hey')) + void psB.publish(topic, uint8ArrayFromString('hey')) - function incMsg (msg) { + function incMsg (msg: Message) { expect(uint8ArrayToString(msg.data)).to.equal('hey') check() } @@ -193,7 +193,7 @@ module.exports = (common) => { } } - return defer.promise + return await defer.promise }) }) }) @@ -206,11 +206,15 @@ module.exports = (common) => { // │b d│ // ◉─┘ └─◉ // a - let psA, psB, psC, psD, psE + let psA: PubSub, psB: PubSub, psC: PubSub, psD: PubSub, psE: PubSub // Create and start pubsub nodes beforeEach(async () => { - [psA, psB, psC, psD, psE] = await common.setup(5) + psA = await common.setup() + psB = await common.setup() + psC = await common.setup() + psD = await common.setup() + psE = await common.setup() // Start pubsub nodes ;[psA, psB, psC, psD, psE].map((p) => p.start()) @@ -218,9 +222,13 @@ module.exports = (common) => { // connect nodes beforeEach(async () => { + // @ts-expect-error protected field await psA._libp2p.dial(psB.peerId) + // @ts-expect-error protected field await psB._libp2p.dial(psC.peerId) + // @ts-expect-error protected field await psC._libp2p.dial(psD.peerId) + // @ts-expect-error protected field await psD._libp2p.dial(psE.peerId) // Wait for peers to be ready in pubsub @@ -277,25 +285,25 @@ module.exports = (common) => { // await a cycle await delay(1000) - psC.publish('Z', uint8ArrayFromString('hey from c')) + void psC.publish('Z', uint8ArrayFromString('hey from c')) - function incMsg (msg) { + function incMsg (msg: Message) { expect(uint8ArrayToString(msg.data)).to.equal('hey from c') check() } function check () { if (++counter === 5) { - psA.unsubscribe('Z', incMsg) - psB.unsubscribe('Z', incMsg) - psC.unsubscribe('Z', incMsg) - psD.unsubscribe('Z', incMsg) - psE.unsubscribe('Z', incMsg) + psA.unsubscribe('Z') + psB.unsubscribe('Z') + psC.unsubscribe('Z') + psD.unsubscribe('Z') + psE.unsubscribe('Z') defer.resolve() } } - return defer.promise + return await defer.promise }) }) }) diff --git a/packages/compliance-tests/src/pubsub/two-nodes.js b/packages/libp2p-interfaces-compliance-tests/src/pubsub/two-nodes.ts similarity index 67% rename from packages/compliance-tests/src/pubsub/two-nodes.js rename to packages/libp2p-interfaces-compliance-tests/src/pubsub/two-nodes.ts index bb35576e0..6ec521443 100644 --- a/packages/compliance-tests/src/pubsub/two-nodes.js +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/two-nodes.ts @@ -1,34 +1,31 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 6] */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const pDefer = require('p-defer') -const pWaitFor = require('p-wait-for') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import pDefer from 'p-defer' +import pWaitFor from 'p-wait-for' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { TestSetup } from '../index.js' +import type { PubSub, Message } from 'libp2p-interfaces/pubsub' +import { first, expectSet -} = require('./utils') +} from './utils.js' const topic = 'foo' -function shouldNotHappen (_) { +function shouldNotHappen () { expect.fail() } -module.exports = (common) => { +export default (common: TestSetup) => { describe('pubsub with two nodes', () => { - let psA, psB + let psA: PubSub, psB: PubSub // Create pubsub nodes and connect them before(async () => { - [psA, psB] = await common.setup(2) + psA = await common.setup() + psB = await common.setup() expect(psA.peers.size).to.be.eql(0) expect(psB.peers.size).to.be.eql(0) @@ -37,6 +34,7 @@ module.exports = (common) => { psA.start() psB.start() + // @ts-expect-error protected property await psA._libp2p.dial(psB.peerId) // Wait for peers to be ready in pubsub @@ -46,13 +44,13 @@ module.exports = (common) => { after(async () => { sinon.restore() - psA && psA.stop() - psB && psB.stop() + psA.stop() + psB.stop() await common.teardown() }) - it('Subscribe to a topic in nodeA', () => { + it('Subscribe to a topic in nodeA', async () => { const defer = pDefer() psB.once('pubsub:subscription-change', (changedPeerId, changedSubs) => { @@ -67,10 +65,10 @@ module.exports = (common) => { }) psA.subscribe(topic) - return defer.promise + return await defer.promise }) - it('Publish to a topic in nodeA', () => { + it('Publish to a topic in nodeA', async () => { const defer = pDefer() psA.once(topic, (msg) => { @@ -81,12 +79,12 @@ module.exports = (common) => { psB.once(topic, shouldNotHappen) - psA.publish(topic, uint8ArrayFromString('hey')) + void psA.publish(topic, uint8ArrayFromString('hey')) - return defer.promise + return await defer.promise }) - it('Publish to a topic in nodeB', () => { + it('Publish to a topic in nodeB', async () => { const defer = pDefer() psA.once(topic, (msg) => { @@ -103,19 +101,19 @@ module.exports = (common) => { psB.once(topic, shouldNotHappen) - psB.publish(topic, uint8ArrayFromString('banana')) + void psB.publish(topic, uint8ArrayFromString('banana')) - return defer.promise + return await defer.promise }) - it('Publish 10 msg to a topic in nodeB', () => { + it('Publish 10 msg to a topic in nodeB', async () => { const defer = pDefer() let counter = 0 psB.once(topic, shouldNotHappen) psA.on(topic, receivedMsg) - function receivedMsg (msg) { + function receivedMsg (msg: Message) { expect(uint8ArrayToString(msg.data)).to.equal('banana') expect(msg.from).to.be.eql(psB.peerId.toB58String()) expect(msg.seqno).to.be.a('Uint8Array') @@ -129,12 +127,12 @@ module.exports = (common) => { } } - Array.from({ length: 10 }, (_, i) => psB.publish(topic, uint8ArrayFromString('banana'))) + Array.from({ length: 10 }, async (_, i) => await psB.publish(topic, uint8ArrayFromString('banana'))) - return defer.promise + return await defer.promise }) - it('Unsubscribe from topic in nodeA', () => { + it('Unsubscribe from topic in nodeA', async () => { const defer = pDefer() psA.unsubscribe(topic) @@ -151,10 +149,10 @@ module.exports = (common) => { defer.resolve() }) - return defer.promise + return await defer.promise }) - it('Publish to a topic:Z in nodeA nodeB', () => { + it('Publish to a topic:Z in nodeA nodeB', async () => { const defer = pDefer() psA.once('Z', shouldNotHappen) @@ -166,10 +164,10 @@ module.exports = (common) => { defer.resolve() }, 100) - psB.publish('Z', uint8ArrayFromString('banana')) - psA.publish('Z', uint8ArrayFromString('banana')) + void psB.publish('Z', uint8ArrayFromString('banana')) + void psA.publish('Z', uint8ArrayFromString('banana')) - return defer.promise + return await defer.promise }) }) } diff --git a/packages/libp2p-interfaces-compliance-tests/src/pubsub/utils.ts b/packages/libp2p-interfaces-compliance-tests/src/pubsub/utils.ts new file mode 100644 index 000000000..7e07146e4 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/pubsub/utils.ts @@ -0,0 +1,13 @@ +import { expect } from 'aegir/utils/chai.js' + +export function first (map: Map): V { + return map.values().next().value +} + +export function expectSet (set?: Set, subs?: T[]) { + if ((set == null) || (subs == null)) { + throw new Error('No set or subs passed') + } + + expect(Array.from(set.values())).to.eql(subs) +} diff --git a/packages/compliance-tests/src/record/index.js b/packages/libp2p-interfaces-compliance-tests/src/record/index.ts similarity index 64% rename from packages/compliance-tests/src/record/index.js rename to packages/libp2p-interfaces-compliance-tests/src/record/index.ts index 532eeda32..fca4dea13 100644 --- a/packages/compliance-tests/src/record/index.js +++ b/packages/libp2p-interfaces-compliance-tests/src/record/index.ts @@ -1,20 +1,18 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ +import { expect } from 'aegir/utils/chai.js' +import type { TestSetup } from '../index.js' +import type { Record } from 'libp2p-interfaces/record' -'use strict' - -const { expect } = require('aegir/utils/chai') - -module.exports = (test) => { +export default (test: TestSetup) => { describe('record', () => { - let record + let record: Record beforeEach(async () => { record = await test.setup() - if (!record) throw new Error('missing record') }) - afterEach(() => test.teardown()) + afterEach(async () => { + await test.teardown() + }) it('has domain and codec', () => { expect(record.domain).to.exist() diff --git a/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/base-test.ts b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/base-test.ts new file mode 100644 index 000000000..625bd0802 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/base-test.ts @@ -0,0 +1,154 @@ +import { expect } from 'aegir/utils/chai.js' +// @ts-expect-error no types +import pair from 'it-pair/duplex.js' +import { pipe } from 'it-pipe' +import { collect, map, consume } from 'streaming-iterables' +import defer from 'p-defer' +import type { TestSetup } from '../index.js' +import type { Muxer, MuxerOptions, MuxedStream } from 'libp2p-interfaces/stream-muxer' +import { isValidTick } from '../transport/utils/index.js' + +function close (stream: MuxedStream) { + return pipe([], stream, consume) +} + +export default (common: TestSetup) => { + describe('base', () => { + it('Open a stream from the dialer', async () => { + const p = pair() + const dialer = await common.setup() + const onStreamPromise: defer.DeferredPromise = defer() + const onStreamEndPromise: defer.DeferredPromise = defer() + + const listener = await common.setup({ + onStream: stream => { + onStreamPromise.resolve(stream) + }, + onStreamEnd: stream => { + onStreamEndPromise.resolve(stream) + } + }) + + pipe(p[0], dialer, p[0]) + pipe(p[1], listener, p[1]) + + const conn = dialer.newStream() + expect(dialer.streams).to.include(conn) + expect(isValidTick(conn.timeline.open)).to.equal(true) + + const stream = await onStreamPromise.promise + expect(isValidTick(stream.timeline.open)).to.equal(true) + // Make sure the stream is being tracked + expect(listener.streams).to.include(stream) + close(stream) + + // Make sure stream is closed properly + const endedStream = await onStreamEndPromise.promise + expect(listener.streams).to.not.include(endedStream) + + if (endedStream.timeline.close == null) { + throw new Error('timeline had no close time') + } + + // Make sure the stream is removed from tracking + expect(isValidTick(endedStream.timeline.close)).to.equal(true) + + await close(conn) + + // ensure we have no streams left + expect(dialer.streams).to.have.length(0) + expect(listener.streams).to.have.length(0) + }) + + it('Open a stream from the listener', async () => { + const p = pair() + const onStreamPromise: defer.DeferredPromise = defer() + const dialer = await common.setup({ + onStream: stream => { + onStreamPromise.resolve(stream) + } + }) + + const listener = await common.setup() + + pipe(p[0], dialer, p[0]) + pipe(p[1], listener, p[1]) + + const conn = listener.newStream() + + const stream = await onStreamPromise.promise + expect(isValidTick(stream.timeline.open)).to.equal(true) + expect(listener.streams).to.include(conn) + expect(isValidTick(conn.timeline.open)).to.equal(true) + await close(stream) + + await close(conn) + }) + + it('Open a stream on both sides', async () => { + const p = pair() + const onDialerStreamPromise: defer.DeferredPromise = defer() + const onListenerStreamPromise: defer.DeferredPromise = defer() + const dialer = await common.setup({ + onStream: stream => { + onDialerStreamPromise.resolve(stream) + } + }) + const listener = await common.setup({ + onStream: stream => { + onListenerStreamPromise.resolve(stream) + } + }) + + pipe(p[0], dialer, p[0]) + pipe(p[1], listener, p[1]) + + const listenerConn = listener.newStream() + const dialerConn = dialer.newStream() + + const dialerStream = await onDialerStreamPromise.promise + const listenerStream = await onListenerStreamPromise.promise + + await close(dialerStream) + await close(listenerStream) + + await close(dialerConn) + await close(listenerConn) + }) + + it('Open a stream on one side, write, open a stream on the other side', async () => { + const toString = map((c: string) => c.slice().toString()) + const p = pair() + const onDialerStreamPromise: defer.DeferredPromise = defer() + const onListenerStreamPromise: defer.DeferredPromise = defer() + const dialer = await common.setup({ + onStream: stream => { + onDialerStreamPromise.resolve(stream) + } + }) + const listener = await common.setup({ + onStream: stream => { + onListenerStreamPromise.resolve(stream) + } + }) + + pipe(p[0], dialer, p[0]) + pipe(p[1], listener, p[1]) + + const dialerConn = dialer.newStream() + const listenerConn = listener.newStream() + + pipe(['hey'], dialerConn) + pipe(['hello'], listenerConn) + + const listenerStream = await onListenerStreamPromise.promise + const dialerStream = await onDialerStreamPromise.promise + + const listenerChunks = await pipe(listenerStream, toString, collect) + expect(listenerChunks).to.be.eql(['hey']) + + const dialerChunks = await pipe(dialerStream, toString, collect) + expect(dialerChunks).to.be.eql(['hello']) + }) + }) +} diff --git a/packages/compliance-tests/src/stream-muxer/close-test.js b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/close-test.ts similarity index 52% rename from packages/compliance-tests/src/stream-muxer/close-test.js rename to packages/libp2p-interfaces-compliance-tests/src/stream-muxer/close-test.ts index c3797ad14..6b3b1b931 100644 --- a/packages/compliance-tests/src/stream-muxer/close-test.js +++ b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/close-test.ts @@ -1,17 +1,17 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 8] */ -'use strict' - -const pair = require('it-pair/duplex') -const { pipe } = require('it-pipe') -const { consume } = require('streaming-iterables') -const { source: abortable } = require('abortable-iterator') -const AbortController = require('abort-controller').default -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -function pause (ms) { - return new Promise(resolve => setTimeout(resolve, ms)) +// @ts-expect-error no types +import pair from 'it-pair/duplex.js' +import { pipe } from 'it-pipe' +import { consume } from 'streaming-iterables' +import { source, duplex } from 'abortable-iterator' +import AbortController from 'abort-controller' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { TestSetup } from '../index.js' +import type { Muxer, MuxerOptions } from 'libp2p-interfaces/stream-muxer' +import type { Connection } from 'libp2p-interfaces/connection' + +async function pause (ms: number) { + return await new Promise(resolve => setTimeout(resolve, ms)) } function randomBuffer () { @@ -27,36 +27,44 @@ const infiniteRandom = { } } -module.exports = (common) => { +export default (common: TestSetup) => { describe('close', () => { - let Muxer - - beforeEach(async () => { - Muxer = await common.setup() - }) - it('closing underlying socket closes streams (tcp)', async () => { - const mockConn = muxer => ({ - newStream: (...args) => muxer.newStream(...args) - }) + const mockConn = (muxer: Muxer): Connection => { + // @ts-expect-error not all Connection methods are implemented + const connection: Connection = { + newStream: async (multicodecs) => { + return { + protocol: multicodecs[0], + stream: muxer.newStream(`${multicodecs[0]}`) + } + } + } - const mockUpgrade = maConn => { - const muxer = new Muxer(stream => pipe(stream, stream)) + return connection + } + + const mockUpgrade = async (maConn: any) => { + const muxer = await common.setup({ + onStream: (stream) => { + pipe(stream, stream) + } + }) pipe(maConn, muxer, maConn) return mockConn(muxer) } const [local, remote] = pair() const controller = new AbortController() - const abortableRemote = abortable.duplex(remote, controller.signal, { + const abortableRemote = duplex(remote, controller.signal, { returnOnAbort: true }) - mockUpgrade(abortableRemote) - const dialerConn = mockUpgrade(local) + await mockUpgrade(abortableRemote) + const dialerConn = await mockUpgrade(local) - const s1 = await dialerConn.newStream() - const s2 = await dialerConn.newStream() + const s1 = await dialerConn.newStream(['']) + const s2 = await dialerConn.newStream(['']) // close the remote in a bit setTimeout(() => controller.abort(), 50) @@ -71,10 +79,12 @@ module.exports = (common) => { it('closing one of the muxed streams doesn\'t close others', async () => { const p = pair() - const dialer = new Muxer() + const dialer = await common.setup() // Listener is echo server :) - const listener = new Muxer(stream => pipe(stream, stream)) + const listener = await common.setup({ + onStream: (stream) => pipe(stream, stream) + }) pipe(p[0], dialer, p[0]) pipe(p[1], listener, p[1]) @@ -82,16 +92,16 @@ module.exports = (common) => { const stream = dialer.newStream() const streams = Array.from(Array(5), () => dialer.newStream()) let closed = false - const controllers = [] + const controllers: AbortController[] = [] const streamResults = streams.map(async stream => { const controller = new AbortController() controllers.push(controller) try { - const abortableRand = abortable(infiniteRandom, controller.signal, { abortCode: 'ERR_TEST_ABORT' }) + const abortableRand = source(infiniteRandom, controller.signal, { abortCode: 'ERR_TEST_ABORT' }) await pipe(abortableRand, stream, consume) - } catch (err) { + } catch (err: any) { if (err.code !== 'ERR_TEST_ABORT') throw err } diff --git a/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/index.ts b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/index.ts new file mode 100644 index 000000000..f48a61a02 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/index.ts @@ -0,0 +1,15 @@ +import baseTest from './base-test.js' +import closeTest from './close-test.js' +import stressTest from './stress-test.js' +import megaStressTest from './mega-stress-test.js' +import type { TestSetup } from '../index.js' +import type { Muxer } from 'libp2p-interfaces/stream-muxer' + +export default (common: TestSetup) => { + describe('interface-stream-muxer', () => { + baseTest(common) + closeTest(common) + stressTest(common) + megaStressTest(common) + }) +} diff --git a/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/mega-stress-test.ts b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/mega-stress-test.ts new file mode 100644 index 000000000..b4195d4f1 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/mega-stress-test.ts @@ -0,0 +1,11 @@ +import spawn from './spawner' +import type { TestSetup } from '../index.js' +import type { Muxer, MuxerOptions } from 'libp2p-interfaces/stream-muxer' + +export default (common: TestSetup) => { + const createMuxer = async (opts?: MuxerOptions) => await common.setup(opts) + + describe.skip('mega stress test', function () { + it('10,000 streams with 10,000 msg', async () => await spawn(createMuxer, 10000, 10000, 5000)) + }) +} diff --git a/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/spawner.ts b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/spawner.ts new file mode 100644 index 000000000..396ade719 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/spawner.ts @@ -0,0 +1,52 @@ +import { expect } from 'aegir/utils/chai.js' +// @ts-expect-error no types +import pair from 'it-pair/duplex.js' +import { pipe } from 'it-pipe' +import pLimit from 'p-limit' +import { collect, consume } from 'streaming-iterables' +import type { Muxer, MuxerOptions } from 'libp2p-interfaces/stream-muxer' + +export default async (createMuxer: (options?: MuxerOptions) => Promise, nStreams: number, nMsg: number, limit?: number) => { + const [dialerSocket, listenerSocket] = pair() + + const msg = 'simple msg' + + const listener = await createMuxer({ + onStream: async (stream) => { + await pipe( + stream, + consume + ) + + pipe([], stream) + } + }) + + const dialer = await createMuxer() + + pipe(listenerSocket, listener, listenerSocket) + pipe(dialerSocket, dialer, dialerSocket) + + const spawnStream = async () => { + const stream = dialer.newStream() + expect(stream).to.exist // eslint-disable-line + + const res = await pipe( + (function * () { + for (let i = 0; i < nMsg; i++) { + yield new Promise(resolve => resolve(msg)) + } + })(), + stream, + collect + ) + + expect(res).to.be.eql([]) + } + + const limiter = pLimit(limit ?? Infinity) + + await Promise.all( + Array.from(Array(nStreams), async () => await limiter(async () => await spawnStream())) + ) +} diff --git a/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/stress-test.ts b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/stress-test.ts new file mode 100644 index 000000000..b1623fc86 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/stream-muxer/stress-test.ts @@ -0,0 +1,24 @@ +import spawn from './spawner' +import type { TestSetup } from '../index.js' +import type { Muxer, MuxerOptions } from 'libp2p-interfaces/stream-muxer' + +export default (common: TestSetup) => { + const createMuxer = async (opts?: MuxerOptions) => await common.setup(opts) + + describe('stress test', () => { + it('1 stream with 1 msg', async () => await spawn(createMuxer, 1, 1)) + it('1 stream with 10 msg', async () => await spawn(createMuxer, 1, 10)) + it('1 stream with 100 msg', async () => await spawn(createMuxer, 1, 100)) + it('10 streams with 1 msg', async () => await spawn(createMuxer, 10, 1)) + it('10 streams with 10 msg', async () => await spawn(createMuxer, 10, 10)) + it('10 streams with 100 msg', async () => await spawn(createMuxer, 10, 100)) + it('100 streams with 1 msg', async () => await spawn(createMuxer, 100, 1)) + it('100 streams with 10 msg', async () => await spawn(createMuxer, 100, 10)) + it('100 streams with 100 msg', async () => await spawn(createMuxer, 100, 100)) + it('1000 streams with 1 msg', async () => await spawn(createMuxer, 1000, 1)) + it('1000 streams with 10 msg', async () => await spawn(createMuxer, 1000, 10)) + it('1000 streams with 100 msg', async function () { + return await spawn(createMuxer, 1000, 100) + }) + }) +} diff --git a/packages/compliance-tests/src/topology/multicodec-topology.js b/packages/libp2p-interfaces-compliance-tests/src/topology/multicodec-topology.ts similarity index 63% rename from packages/compliance-tests/src/topology/multicodec-topology.js rename to packages/libp2p-interfaces-compliance-tests/src/topology/multicodec-topology.ts index 1a87ac63b..d561a857c 100644 --- a/packages/compliance-tests/src/topology/multicodec-topology.js +++ b/packages/libp2p-interfaces-compliance-tests/src/topology/multicodec-topology.ts @@ -1,24 +1,19 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const PeerId = require('peer-id') - -const peers = require('../utils/peers') - -module.exports = (test) => { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import PeerIdFactory from 'peer-id' +import peers from '../utils/peers.js' +import type { TestSetup } from '../index.js' +import type { MulticodecTopology } from 'libp2p-interfaces/topology' +import type { PeerId } from 'libp2p-interfaces/peer-id' + +export default (test: TestSetup) => { describe('multicodec topology', () => { - let topology, id + let topology: MulticodecTopology, id: PeerId beforeEach(async () => { topology = await test.setup() - if (!topology) throw new Error('missing multicodec topology') - id = await PeerId.createFromJSON(peers[0]) + id = await PeerIdFactory.createFromJSON(peers[0]) }) afterEach(async () => { @@ -28,26 +23,26 @@ module.exports = (test) => { it('should have properties set', () => { expect(topology.multicodecs).to.exist() - expect(topology._onConnect).to.exist() - expect(topology._onDisconnect).to.exist() expect(topology.peers).to.exist() - expect(topology._registrar).to.exist() }) it('should trigger "onDisconnect" on peer disconnected', () => { + // @ts-expect-error protected property sinon.spy(topology, '_onDisconnect') topology.disconnect(id) - expect(topology._onDisconnect.callCount).to.equal(1) + expect(topology).to.have.nested.property('_onDisconnect.callCount', 1) }) it('should update peers on protocol change', async () => { + // @ts-expect-error protected property sinon.spy(topology, '_updatePeers') expect(topology.peers.size).to.eql(0) + // @ts-expect-error protected property const peerStore = topology._registrar.peerStore - const id2 = await PeerId.createFromJSON(peers[1]) + const id2 = await PeerIdFactory.createFromJSON(peers[1]) peerStore.peers.set(id2.toB58String(), { id: id2, protocols: Array.from(topology.multicodecs) @@ -58,17 +53,19 @@ module.exports = (test) => { protocols: Array.from(topology.multicodecs) }) - expect(topology._updatePeers.callCount).to.equal(1) + expect(topology).to.have.nested.property('_updatePeers.callCount', 1) expect(topology.peers.size).to.eql(1) }) it('should disconnect if peer no longer supports a protocol', async () => { + // @ts-expect-error protected property sinon.spy(topology, '_onDisconnect') expect(topology.peers.size).to.eql(0) + // @ts-expect-error protected property const peerStore = topology._registrar.peerStore - const id2 = await PeerId.createFromJSON(peers[1]) + const id2 = await PeerIdFactory.createFromJSON(peers[1]) peerStore.peers.set(id2.toB58String(), { id: id2, protocols: Array.from(topology.multicodecs) @@ -92,41 +89,48 @@ module.exports = (test) => { }) expect(topology.peers.size).to.eql(1) - expect(topology._onDisconnect.callCount).to.equal(1) + expect(topology).to.have.nested.property('_onDisconnect.callCount', 1) + // @ts-expect-error protected property expect(topology._onDisconnect.calledWith(id2)).to.equal(true) }) it('should trigger "onConnect" when a peer connects and has one of the topology multicodecs in its known protocols', () => { + // @ts-expect-error protected property sinon.spy(topology, '_onConnect') + // @ts-expect-error protected property sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns(topology.multicodecs) - + // @ts-expect-error protected property topology._registrar.connectionManager.emit('peer:connect', { remotePeer: id }) - expect(topology._onConnect.callCount).to.equal(1) + expect(topology).to.have.nested.property('_onConnect.callCount', 1) }) it('should not trigger "onConnect" when a peer connects and has none of the topology multicodecs in its known protocols', () => { + // @ts-expect-error protected property sinon.spy(topology, '_onConnect') + // @ts-expect-error protected property sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns([]) - + // @ts-expect-error protected property topology._registrar.connectionManager.emit('peer:connect', { remotePeer: id }) - expect(topology._onConnect.callCount).to.equal(0) + expect(topology).to.have.nested.property('_onConnect.callCount', 0) }) it('should not trigger "onConnect" when a peer connects and its protocols are not known', () => { + // @ts-expect-error protected property sinon.spy(topology, '_onConnect') + // @ts-expect-error protected property sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns(undefined) - + // @ts-expect-error protected property topology._registrar.connectionManager.emit('peer:connect', { remotePeer: id }) - expect(topology._onConnect.callCount).to.equal(0) + expect(topology).to.have.nested.property('_onConnect.callCount', 0) }) }) } diff --git a/packages/libp2p-interfaces-compliance-tests/src/topology/topology.ts b/packages/libp2p-interfaces-compliance-tests/src/topology/topology.ts new file mode 100644 index 000000000..28959dcea --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/topology/topology.ts @@ -0,0 +1,38 @@ +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import PeerIdFactory from 'peer-id' +import peers from '../utils/peers.js' +import type { TestSetup } from '../index.js' +import type { Topology } from 'libp2p-interfaces/topology' +import type { PeerId } from 'libp2p-interfaces/peer-id' + +export default (test: TestSetup) => { + describe('topology', () => { + let topology: Topology, id: PeerId + + beforeEach(async () => { + topology = await test.setup() + + id = await PeerIdFactory.createFromJSON(peers[0]) + }) + + afterEach(async () => { + sinon.restore() + await test.teardown() + }) + + it('should have properties set', () => { + expect(topology.min).to.exist() + expect(topology.max).to.exist() + expect(topology.peers).to.exist() + }) + + it('should trigger "onDisconnect" on peer disconnected', () => { + // @ts-expect-error protected property + sinon.spy(topology, '_onDisconnect') + topology.disconnect(id) + + expect(topology).to.have.nested.property('_onDisconnect.callCount', 1) + }) + }) +} diff --git a/packages/compliance-tests/src/transport/dial-test.js b/packages/libp2p-interfaces-compliance-tests/src/transport/dial-test.ts similarity index 71% rename from packages/compliance-tests/src/transport/dial-test.js rename to packages/libp2p-interfaces-compliance-tests/src/transport/dial-test.ts index 5c9de5df5..a2237bb60 100644 --- a/packages/compliance-tests/src/transport/dial-test.js +++ b/packages/libp2p-interfaces-compliance-tests/src/transport/dial-test.ts @@ -1,53 +1,41 @@ -// @ts-nocheck interface tests -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const { isValidTick } = require('./utils') -const goodbye = require('it-goodbye') -const { collect } = require('streaming-iterables') -const { pipe } = require('it-pipe') -const AbortController = require('abort-controller').default -const { AbortError } = require('libp2p-interfaces/src/transport/errors') -const sinon = require('sinon') - -module.exports = (common) => { - const upgrader = { - _upgrade (multiaddrConnection) { - ['sink', 'source', 'remoteAddr', 'conn', 'timeline', 'close'].forEach(prop => { - expect(multiaddrConnection).to.have.property(prop) - }) - expect(isValidTick(multiaddrConnection.timeline.open)).to.equal(true) - return multiaddrConnection - }, - upgradeOutbound (multiaddrConnection) { - return upgrader._upgrade(multiaddrConnection) - }, - upgradeInbound (multiaddrConnection) { - return upgrader._upgrade(multiaddrConnection) - } - } - +import { expect } from 'aegir/utils/chai.js' +import { isValidTick, mockUpgrader } from './utils/index.js' +// @ts-expect-error no types +import goodbye from 'it-goodbye' +import { collect } from 'streaming-iterables' +import { pipe } from 'it-pipe' +import AbortController from 'abort-controller' +import { AbortError } from 'libp2p-interfaces/transport/errors' +import sinon from 'sinon' +import type { TestSetup } from '../index.js' +import type { Transport, Listener } from 'libp2p-interfaces/transport' +import type { TransportTestFixtures, SetupArgs, Connector } from './index.js' +import type { Multiaddr } from 'multiaddr' + +export default (common: TestSetup) => { describe('dial', () => { - let addrs - let transport - let connector - let listener + const upgrader = mockUpgrader() + let addrs: Multiaddr[] + let transport: Transport + let connector: Connector + let listener: Listener before(async () => { ({ addrs, transport, connector } = await common.setup({ upgrader })) }) - after(() => common.teardown && common.teardown()) + after(async () => { + await common.teardown() + }) - beforeEach(() => { - listener = transport.createListener((conn) => pipe(conn, conn)) - return listener.listen(addrs[0]) + beforeEach(async () => { + listener = transport.createListener({}, (conn) => pipe(conn, conn)) + return await listener.listen(addrs[0]) }) - afterEach(() => { + afterEach(async () => { sinon.restore() - return listener.close() + return await listener.close() }) it('simple', async () => { @@ -59,6 +47,7 @@ module.exports = (common) => { const result = await pipe(s, conn, s) expect(upgradeSpy.callCount).to.equal(1) + // @ts-expect-error upgrader.upgradeOutbound returns a Connection, not Promise expect(upgradeSpy.returned(conn)).to.equal(true) expect(result.length).to.equal(1) expect(result[0].toString()).to.equal('hey') @@ -69,9 +58,10 @@ module.exports = (common) => { const conn = await transport.dial(addrs[0]) expect(upgradeSpy.callCount).to.equal(1) + // @ts-expect-error upgrader.upgradeOutbound returns a Connection, not Promise expect(upgradeSpy.returned(conn)).to.equal(true) await conn.close() - expect(isValidTick(conn.timeline.close)).to.equal(true) + expect(isValidTick(conn.stat.timeline.close)).to.equal(true) }) it('to non existent listener', async () => { @@ -94,7 +84,7 @@ module.exports = (common) => { try { await socket - } catch (err) { + } catch (err: any) { expect(upgradeSpy.callCount).to.equal(0) expect(err.code).to.eql(AbortError.code) expect(err.type).to.eql(AbortError.type) @@ -115,7 +105,7 @@ module.exports = (common) => { try { await socket - } catch (err) { + } catch (err: any) { expect(upgradeSpy.callCount).to.equal(0) expect(err.code).to.eql(AbortError.code) expect(err.type).to.eql(AbortError.type) @@ -128,14 +118,14 @@ module.exports = (common) => { it('abort while reading throws AbortError', async () => { // Add a delay to the response from the server - async function * delayedResponse (source) { + async function * delayedResponse (source: AsyncIterable) { for await (const val of source) { await new Promise((resolve) => setTimeout(resolve, 1000)) yield val } } - const delayedListener = transport.createListener(async (conn) => { - await pipe(conn, delayedResponse, conn) + const delayedListener = transport.createListener({}, (conn) => { + void pipe(conn, delayedResponse, conn) }) await delayedListener.listen(addrs[1]) @@ -150,7 +140,7 @@ module.exports = (common) => { // An AbortError should be thrown before the pipe completes const s = goodbye({ source: ['hey'], sink: collect }) await pipe(s, socket, s) - } catch (err) { + } catch (err: any) { expect(err.code).to.eql(AbortError.code) expect(err.type).to.eql(AbortError.type) return @@ -162,15 +152,15 @@ module.exports = (common) => { it('abort while writing does not throw AbortError', async () => { // Record values received by the listener - const recorded = [] - async function * recorderTransform (source) { + const recorded: string[] = [] + async function * recorderTransform (source: AsyncIterable) { for await (const val of source) { recorded.push(val) yield val } } - const recordListener = transport.createListener(async (conn) => { - await pipe(conn, recorderTransform, conn) + const recordListener = transport.createListener({}, (conn) => { + void pipe(conn, recorderTransform, conn) }) await recordListener.listen(addrs[1]) diff --git a/packages/libp2p-interfaces-compliance-tests/src/transport/filter-test.ts b/packages/libp2p-interfaces-compliance-tests/src/transport/filter-test.ts new file mode 100644 index 000000000..9bec6d6a6 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/transport/filter-test.ts @@ -0,0 +1,26 @@ +import { expect } from 'aegir/utils/chai.js' +import { mockUpgrader } from './utils/index.js' +import type { TestSetup } from '../index.js' +import type { Transport } from 'libp2p-interfaces/transport' +import type { TransportTestFixtures, SetupArgs } from './index.js' +import type { Multiaddr } from 'multiaddr' + +export default (common: TestSetup) => { + describe('filter', () => { + let addrs: Multiaddr[] + let transport: Transport + + before(async () => { + ({ addrs, transport } = await common.setup({ upgrader: mockUpgrader() })) + }) + + after(async () => { + await common.teardown() + }) + + it('filters addresses', () => { + const filteredAddrs = transport.filter(addrs) + expect(filteredAddrs).to.eql(addrs) + }) + }) +} diff --git a/packages/libp2p-interfaces-compliance-tests/src/transport/index.ts b/packages/libp2p-interfaces-compliance-tests/src/transport/index.ts new file mode 100644 index 000000000..6641c2464 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/transport/index.ts @@ -0,0 +1,29 @@ +import dial from './dial-test.js' +import listen from './listen-test.js' +import filter from './filter-test.js' +import type { TestSetup } from '../index.js' +import type { Transport, Upgrader } from 'libp2p-interfaces/transport' +import type { Multiaddr } from 'multiaddr' + +export interface Connector { + delay: (ms: number) => void + restore: () => void +} + +export interface TransportTestFixtures { + addrs: Multiaddr[] + transport: Transport<{}, {}> + connector: Connector +} + +export interface SetupArgs { + upgrader: Upgrader +} + +export default (common: TestSetup) => { + describe('interface-transport', () => { + dial(common) + listen(common) + filter(common) + }) +} diff --git a/packages/compliance-tests/src/transport/listen-test.js b/packages/libp2p-interfaces-compliance-tests/src/transport/listen-test.ts similarity index 57% rename from packages/compliance-tests/src/transport/listen-test.js rename to packages/libp2p-interfaces-compliance-tests/src/transport/listen-test.ts index c6989f5f5..35612b56e 100644 --- a/packages/compliance-tests/src/transport/listen-test.js +++ b/packages/libp2p-interfaces-compliance-tests/src/transport/listen-test.ts @@ -1,60 +1,47 @@ -// @ts-nocheck interface tests /* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const pWaitFor = require('p-wait-for') -const { pipe } = require('it-pipe') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const { isValidTick } = require('./utils') - -module.exports = (common) => { - const upgrader = { - _upgrade (multiaddrConnection) { - ['sink', 'source', 'remoteAddr', 'conn', 'timeline', 'close'].forEach(prop => { - expect(multiaddrConnection).to.have.property(prop) - }) - expect(isValidTick(multiaddrConnection.timeline.open)).to.equal(true) - - return multiaddrConnection - }, - upgradeOutbound (multiaddrConnection) { - return upgrader._upgrade(multiaddrConnection) - }, - upgradeInbound (multiaddrConnection) { - return upgrader._upgrade(multiaddrConnection) - } - } - +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import pWaitFor from 'p-wait-for' +import { pipe } from 'it-pipe' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { isValidTick, mockUpgrader } from './utils/index.js' +import type { TestSetup } from '../index.js' +import type { Transport } from 'libp2p-interfaces/transport' +import type { TransportTestFixtures, SetupArgs } from './index.js' +import type { Multiaddr } from 'multiaddr' +import type { Connection } from 'libp2p-interfaces/connection' + +export default (common: TestSetup) => { describe('listen', () => { - let addrs - let transport + const upgrader = mockUpgrader() + let addrs: Multiaddr[] + let transport: Transport before(async () => { ({ transport, addrs } = await common.setup({ upgrader })) }) - after(() => common.teardown && common.teardown()) + after(async () => { + await common.teardown() + }) afterEach(() => { sinon.restore() }) it('simple', async () => { - const listener = transport.createListener((conn) => {}) + const listener = transport.createListener({}, (conn) => {}) await listener.listen(addrs[0]) await listener.close() }) it('close listener with connections, through timeout', async () => { const upgradeSpy = sinon.spy(upgrader, 'upgradeInbound') - const listenerConns = [] + const listenerConns: Connection[] = [] - const listener = transport.createListener((conn) => { + const listener = transport.createListener({}, (conn) => { listenerConns.push(conn) + // @ts-expect-error upgrader.upgradeOutbound returns a Connection, not Promise expect(upgradeSpy.returned(conn)).to.equal(true) pipe(conn, conn) }) @@ -83,9 +70,9 @@ module.exports = (common) => { await socket1.close() - expect(isValidTick(socket1.timeline.close)).to.equal(true) + expect(isValidTick(socket1.stat.timeline.close)).to.equal(true) listenerConns.forEach(conn => { - expect(isValidTick(conn.timeline.close)).to.equal(true) + expect(isValidTick(conn.stat.timeline.close)).to.equal(true) }) // 2 dials = 2 connections upgraded @@ -105,53 +92,50 @@ module.exports = (common) => { // Create a connection to the listener const socket = await transport.dial(addrs[0]) - await pWaitFor(() => typeof socket.timeline.close === 'number') + await pWaitFor(() => typeof socket.stat.timeline.close === 'number') await listener.close() }) describe('events', () => { it('connection', (done) => { const upgradeSpy = sinon.spy(upgrader, 'upgradeInbound') - const listener = transport.createListener() + const listener = transport.createListener({}) - listener.on('connection', async (conn) => { + listener.on('connection', (conn) => { expect(upgradeSpy.returned(conn)).to.equal(true) expect(upgradeSpy.callCount).to.equal(1) expect(conn).to.exist() - await listener.close() - done() + listener.close().then(done, done) }) - ;(async () => { + void (async () => { await listener.listen(addrs[0]) await transport.dial(addrs[0]) })() }) it('listening', (done) => { - const listener = transport.createListener() - listener.on('listening', async () => { - await listener.close() - done() + const listener = transport.createListener({}) + listener.on('listening', () => { + listener.close().then(done, done) }) - listener.listen(addrs[0]) + void listener.listen(addrs[0]) }) it('error', (done) => { - const listener = transport.createListener() - listener.on('error', async (err) => { + const listener = transport.createListener({}) + listener.on('error', (err) => { expect(err).to.exist() - await listener.close() - done() + listener.close().then(done, done) }) listener.emit('error', new Error('my err')) }) it('close', (done) => { - const listener = transport.createListener() + const listener = transport.createListener({}) listener.on('close', done) - ;(async () => { + void (async () => { await listener.listen(addrs[0]) await listener.close() })() diff --git a/packages/libp2p-interfaces-compliance-tests/src/transport/utils/index.ts b/packages/libp2p-interfaces-compliance-tests/src/transport/utils/index.ts new file mode 100644 index 000000000..8945fc87f --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/src/transport/utils/index.ts @@ -0,0 +1,42 @@ +import { expect } from 'aegir/utils/chai.js' +import type { Upgrader, MultiaddrConnection } from 'libp2p-interfaces/transport' + +/** + * A tick is considered valid if it happened between now + * and `ms` milliseconds ago + */ +export function isValidTick (date?: number, ms: number = 5000) { + if (date == null) { + throw new Error('date must be a number') + } + + const now = Date.now() + + if (date > now - ms && date <= now) { + return true + } + + return false +} + +export function mockUpgrader () { + const _upgrade = async (multiaddrConnection: MultiaddrConnection) => { + ['sink', 'source', 'remoteAddr', 'conn', 'timeline', 'close'].forEach(prop => { + expect(multiaddrConnection).to.have.property(prop) + }) + expect(isValidTick(multiaddrConnection.timeline.open)).to.equal(true) + return multiaddrConnection + } + const upgrader: Upgrader = { + // @ts-expect-error we return a MultiaddrConnetion that is not a Connection + async upgradeOutbound (multiaddrConnection) { + return await _upgrade(multiaddrConnection) + }, + // @ts-expect-error we return a MultiaddrConnetion that is not a Connection + async upgradeInbound (multiaddrConnection) { + return await _upgrade(multiaddrConnection) + } + } + + return upgrader +} diff --git a/packages/compliance-tests/src/utils/peers.js b/packages/libp2p-interfaces-compliance-tests/src/utils/peers.ts similarity index 99% rename from packages/compliance-tests/src/utils/peers.js rename to packages/libp2p-interfaces-compliance-tests/src/utils/peers.ts index fad0d23e8..42bacedea 100644 --- a/packages/compliance-tests/src/utils/peers.js +++ b/packages/libp2p-interfaces-compliance-tests/src/utils/peers.ts @@ -1,6 +1,4 @@ -'use strict' - -module.exports = [{ +export default [{ id: 'QmNMMAqSxPetRS1cVMmutW5BCN1qQQyEr4u98kUvZjcfEw', privKey: 'CAASpQkwggShAgEAAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAECggEAB2H2uPRoRCAKU+T3gO4QeoiJaYKNjIO7UCplE0aMEeHDnEjAKC1HQ1G0DRdzZ8sb0fxuIGlNpFMZv5iZ2ZFg2zFfV//DaAwTek9tIOpQOAYHUtgHxkj5FIlg2BjlflGb+ZY3J2XsVB+2HNHkUEXOeKn2wpTxcoJE07NmywkO8Zfr1OL5oPxOPlRN1gI4ffYH2LbfaQVtRhwONR2+fs5ISfubk5iKso6BX4moMYkxubYwZbpucvKKi/rIjUA3SK86wdCUnno1KbDfdXSgCiUlvxt/IbRFXFURQoTV6BOi3sP5crBLw8OiVubMr9/8WE6KzJ0R7hPd5+eeWvYiYnWj4QKBgQD6jRlAFo/MgPO5NZ/HRAk6LUG+fdEWexA+GGV7CwJI61W/Dpbn9ZswPDhRJKo3rquyDFVZPdd7+RlXYg1wpmp1k54z++L1srsgj72vlg4I8wkZ4YLBg0+zVgHlQ0kxnp16DvQdOgiRFvMUUMEgetsoIx1CQWTd67hTExGsW+WAZQKBgQDT/WaHWvwyq9oaZ8G7F/tfeuXvNTk3HIJdfbWGgRXB7lJ7Gf6FsX4x7PeERfL5a67JLV6JdiLLVuYC2CBhipqLqC2DB962aKMvxobQpSljBBZvZyqP1IGPoKskrSo+2mqpYkeCLbDMuJ1nujgMP7gqVjabs2zj6ACKmmpYH/oNowJ/T0ZVtvFsjkg+1VsiMupUARRQuPUWMwa9HOibM1NIZcoQV2NGXB5Z++kR6JqxQO0DZlKArrviclderUdY+UuuY4VRiSEprpPeoW7ZlbTku/Ap8QZpWNEzZorQDro7bnfBW91fX9/81ets/gCPGrfEn+58U3pdb9oleCOQc/ifpQKBgBTYGbi9bYbd9vgZs6bd2M2um+VFanbMytS+g5bSIn2LHXkVOT2UEkB+eGf9KML1n54QY/dIMmukA8HL1oNAyalpw+/aWj+9Ui5kauUhGEywHjSeBEVYM9UXizxz+m9rsoktLLLUI0o97NxCJzitG0Kub3gn0FEogsUeIc7AdinZAoGBANnM1vcteSQDs7x94TDEnvvqwSkA2UWyLidD2jXgE0PG4V6tTkK//QPBmC9eq6TIqXkzYlsErSw4XeKO91knFofmdBzzVh/ddgx/NufJV4tXF+a2iTpqYBUJiz9wpIKgf43/Ob+P1EA99GAhSdxz1ess9O2aTqf3ANzn6v6g62Pv', pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAE=' diff --git a/packages/compliance-tests/test/connection/index.spec.js b/packages/libp2p-interfaces-compliance-tests/test/connection/index.spec.ts similarity index 50% rename from packages/compliance-tests/test/connection/index.spec.js rename to packages/libp2p-interfaces-compliance-tests/test/connection/index.spec.ts index 3d37c5e3d..58a432de4 100644 --- a/packages/compliance-tests/test/connection/index.spec.js +++ b/packages/libp2p-interfaces-compliance-tests/test/connection/index.spec.ts @@ -1,29 +1,26 @@ -/* eslint-env mocha */ -'use strict' - -const tests = require('../../src/connection') -const { Connection } = require('libp2p-interfaces/src/connection') -const peers = require('libp2p-interfaces/test/utils/peers') -const PeerId = require('peer-id') -const { Multiaddr } = require('multiaddr') -const pair = require('it-pair') +import tests from '../../src/connection/index.js' +import { Connection } from 'libp2p-connection' +import peers from '../../src/utils/peers.js' +import PeerIdFactory from 'peer-id' +import { Multiaddr } from 'multiaddr' +// @ts-expect-error no types +import pair from 'it-pair' +import type { MuxedStream } from 'libp2p-interfaces/stream-muxer' describe('compliance tests', () => { tests({ /** * Test setup. `properties` allows the compliance test to override * certain values for testing. - * - * @param {*} properties */ async setup (properties) { const localAddr = new Multiaddr('/ip4/127.0.0.1/tcp/8080') const remoteAddr = new Multiaddr('/ip4/127.0.0.1/tcp/8081') const [localPeer, remotePeer] = await Promise.all([ - PeerId.createFromJSON(peers[0]), - PeerId.createFromJSON(peers[1]) + PeerIdFactory.createFromJSON(peers[0]), + PeerIdFactory.createFromJSON(peers[1]) ]) - const openStreams = [] + const openStreams: MuxedStream[] = [] let streamId = 0 const connection = new Connection({ @@ -38,17 +35,25 @@ describe('compliance tests', () => { }, direction: 'outbound', encryption: '/secio/1.0.0', - multiplexer: '/mplex/6.7.0' + multiplexer: '/mplex/6.7.0', + status: 'OPEN' }, - newStream: (protocols) => { - const id = streamId++ - const stream = pair() - - stream.close = async () => { - await stream.sink([]) - connection.removeStream(stream.id) + newStream: async (protocols) => { + const id = `${streamId++}` + const stream: MuxedStream = { + ...pair(), + close: async () => { + await stream.sink([]) + connection.removeStream(stream.id) + }, + id, + abort: () => {}, + reset: () => {}, + timeline: { + open: 0 + }, + [Symbol.asyncIterator]: () => stream.source } - stream.id = id openStreams.push(stream) @@ -57,7 +62,7 @@ describe('compliance tests', () => { protocol: protocols[0] } }, - close: () => {}, + close: async () => {}, getStreams: () => openStreams, ...properties }) diff --git a/packages/libp2p-interfaces-compliance-tests/test/crypto/index.spec.ts b/packages/libp2p-interfaces-compliance-tests/test/crypto/index.spec.ts new file mode 100644 index 000000000..f97383c7f --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/test/crypto/index.spec.ts @@ -0,0 +1,11 @@ +import tests from '../../src/crypto/index.js' +import mockCrypto from './mock-crypto.js' + +describe('compliance tests', () => { + tests({ + async setup () { + return mockCrypto + }, + async teardown () {} + }) +}) diff --git a/packages/libp2p-interfaces-compliance-tests/test/crypto/mock-crypto.ts b/packages/libp2p-interfaces-compliance-tests/test/crypto/mock-crypto.ts new file mode 100644 index 000000000..dae1ae10a --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/test/crypto/mock-crypto.ts @@ -0,0 +1,109 @@ +import PeerIdFactory from 'peer-id' +// @ts-expect-error no types +import handshake from 'it-handshake' +// @ts-expect-error no types +import duplexPair from 'it-pair/duplex.js' +import pipe from 'it-pipe' +import { UnexpectedPeerError } from 'libp2p-interfaces/crypto/errors' +import type { Crypto } from 'libp2p-interfaces/crypto' +import { Multiaddr } from 'multiaddr' + +// A basic transform that does nothing to the data +const transform = () => { + return (source: AsyncIterable) => (async function * () { + for await (const chunk of source) { + yield chunk + } + })() +} + +const crypto: Crypto = { + protocol: 'insecure', + secureInbound: async (localPeer, duplex, expectedPeer) => { + // 1. Perform a basic handshake. + const shake = handshake(duplex) + shake.write(localPeer.id) + const remoteId = await shake.read() + + if (remoteId == null) { + throw new Error('Could not read remote ID') + } + + const remotePeer = PeerIdFactory.createFromBytes(remoteId.slice()) + shake.rest() + + if ((expectedPeer != null) && expectedPeer.id !== remotePeer.id) { + throw new UnexpectedPeerError() + } + + // 2. Create your encryption box/unbox wrapper + const wrapper = duplexPair() + const encrypt = transform() // Use transform iterables to modify data + const decrypt = transform() + + pipe( + wrapper[0], // We write to wrapper + encrypt, // The data is encrypted + shake.stream, // It goes to the remote peer + decrypt, // Decrypt the incoming data + wrapper[0] // Pipe to the wrapper + ) + + return { + conn: { + ...wrapper[1], + close: async () => {}, + localAddr: new Multiaddr('/ip4/127.0.0.1/tcp/4001'), + remoteAddr: new Multiaddr('/ip4/127.0.0.1/tcp/4002'), + timeline: { + open: Date.now() + }, + conn: true + }, + remotePeer, + remoteEarlyData: new Uint8Array(0) + } + }, + secureOutbound: async (localPeer, duplex, remotePeer) => { + // 1. Perform a basic handshake. + const shake = handshake(duplex) + shake.write(localPeer.id) + const remoteId = await shake.read() + + if (remoteId == null) { + throw new Error('Could not read remote ID') + } + + shake.rest() + + // 2. Create your encryption box/unbox wrapper + const wrapper = duplexPair() + const encrypt = transform() + const decrypt = transform() + + pipe( + wrapper[0], // We write to wrapper + encrypt, // The data is encrypted + shake.stream, // It goes to the remote peer + decrypt, // Decrypt the incoming data + wrapper[0] // Pipe to the wrapper + ) + + return { + conn: { + ...wrapper[1], + close: async () => {}, + localAddr: new Multiaddr('/ip4/127.0.0.1/tcp/4001'), + remoteAddr: new Multiaddr('/ip4/127.0.0.1/tcp/4002'), + timeline: { + open: Date.now() + }, + conn: true + }, + remotePeer: PeerIdFactory.createFromBytes(remoteId.slice()), + remoteEarlyData: new Uint8Array(0) + } + } +} + +export default crypto diff --git a/packages/compliance-tests/test/peer-discovery/index.spec.js b/packages/libp2p-interfaces-compliance-tests/test/peer-discovery/index.spec.ts similarity index 59% rename from packages/compliance-tests/test/peer-discovery/index.spec.js rename to packages/libp2p-interfaces-compliance-tests/test/peer-discovery/index.spec.ts index 5102f0b93..fa248ce33 100644 --- a/packages/compliance-tests/test/peer-discovery/index.spec.js +++ b/packages/libp2p-interfaces-compliance-tests/test/peer-discovery/index.spec.ts @@ -1,14 +1,11 @@ -/* eslint-env mocha */ -'use strict' - -const tests = require('../../src/peer-discovery') -const MockDiscovery = require('./mock-discovery') +import tests from '../../src/peer-discovery/index.js' +import { MockDiscovery } from './mock-discovery.js' describe('compliance tests', () => { - let intervalId + let intervalId: any tests({ - setup () { + async setup () { const mockDiscovery = new MockDiscovery({ discoveryDelay: 1 }) @@ -17,7 +14,7 @@ describe('compliance tests', () => { return mockDiscovery }, - teardown () { + async teardown () { clearInterval(intervalId) } }) diff --git a/packages/libp2p-interfaces-compliance-tests/test/peer-discovery/mock-discovery.ts b/packages/libp2p-interfaces-compliance-tests/test/peer-discovery/mock-discovery.ts new file mode 100644 index 000000000..849755a46 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/test/peer-discovery/mock-discovery.ts @@ -0,0 +1,48 @@ +import { EventEmitter } from 'events' +import { Multiaddr } from 'multiaddr' +import PeerIdFactory from 'peer-id' + +interface MockDiscoveryOptions { + discoveryDelay?: number +} + +/** + * Emits 'peer' events on discovery. + */ +export class MockDiscovery extends EventEmitter { + public readonly options: MockDiscoveryOptions + private _isRunning: boolean + private _timer: any + + constructor (options = {}) { + super() + + this.options = options + this._isRunning = false + } + + start () { + this._isRunning = true + this._discoverPeer() + } + + stop () { + clearTimeout(this._timer) + this._isRunning = false + } + + _discoverPeer () { + if (!this._isRunning) return + + PeerIdFactory.create({ bits: 512 }) + .then(peerId => { + this._timer = setTimeout(() => { + this.emit('peer', { + id: peerId, + multiaddrs: [new Multiaddr('/ip4/127.0.0.1/tcp/8000')] + }) + }, this.options.discoveryDelay ?? 1000) + }) + .catch(() => {}) + } +} diff --git a/packages/libp2p-interfaces-compliance-tests/test/topology/mock-peer-store.ts b/packages/libp2p-interfaces-compliance-tests/test/topology/mock-peer-store.ts new file mode 100644 index 000000000..0bd5bc59e --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/test/topology/mock-peer-store.ts @@ -0,0 +1,27 @@ +import { EventEmitter } from 'events' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { PeerData } from 'libp2p-interfaces/peer-data' +import type { ProtoBook, PeerStore } from 'libp2p-interfaces/registrar' + +export class MockPeerStore extends EventEmitter implements PeerStore { + public readonly peers: Map + public protoBook: ProtoBook + + constructor (peers: Map) { + super() + this.protoBook = { + get: () => ([]) + } + this.peers = peers + } + + get (peerId: PeerId) { + const peerData = this.peers.get(peerId.toB58String()) + + if (peerData == null) { + throw new Error('PeerData not found') + } + + return peerData + } +} diff --git a/packages/libp2p-interfaces-compliance-tests/test/topology/multicodec-topology.spec.ts b/packages/libp2p-interfaces-compliance-tests/test/topology/multicodec-topology.spec.ts new file mode 100644 index 000000000..4462c1fda --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/test/topology/multicodec-topology.spec.ts @@ -0,0 +1,50 @@ +import { EventEmitter } from 'events' +import tests from '../../src/topology/multicodec-topology.js' +import { MulticodecTopology } from 'libp2p-topology/multicodec-topology' +import { MockPeerStore } from './mock-peer-store.js' + +describe('multicodec topology compliance tests', () => { + tests({ + async setup (properties) { + const multicodecs = ['/echo/1.0.0'] + const handlers = { + onConnect: () => { }, + onDisconnect: () => { } + } + + const topology = new MulticodecTopology({ + multicodecs, + handlers, + ...properties + }) + + const peers = new Map() + const peerStore = new MockPeerStore(peers) + const connectionManager = new EventEmitter() + + const registrar = { + peerStore, + connectionManager, + getConnection: () => { + return undefined + }, + handle: () => { + throw new Error('Not implemented') + }, + register: () => { + throw new Error('Not implemented') + }, + unregister: () => { + throw new Error('Not implemented') + } + } + + topology.registrar = registrar + + return topology + }, + async teardown () { + // cleanup resources created by setup() + } + }) +}) diff --git a/packages/compliance-tests/test/topology/topology.spec.js b/packages/libp2p-interfaces-compliance-tests/test/topology/topology.spec.ts similarity index 63% rename from packages/compliance-tests/test/topology/topology.spec.js rename to packages/libp2p-interfaces-compliance-tests/test/topology/topology.spec.ts index 9ba839fb9..97f2fb60a 100644 --- a/packages/compliance-tests/test/topology/topology.spec.js +++ b/packages/libp2p-interfaces-compliance-tests/test/topology/topology.spec.ts @@ -1,12 +1,9 @@ -/* eslint-env mocha */ -'use strict' - -const tests = require('../../src/topology/topology') -const Topology = require('libp2p-interfaces/src/topology') +import tests from '../../src/topology/topology.js' +import { Topology } from 'libp2p-topology' describe('topology compliance tests', () => { tests({ - setup (properties) { + async setup (properties) { const handlers = { onConnect: () => { }, onDisconnect: () => { } @@ -19,7 +16,7 @@ describe('topology compliance tests', () => { return topology }, - teardown () { + async teardown () { // cleanup resources created by setup() } }) diff --git a/packages/libp2p-interfaces-compliance-tests/tsconfig.json b/packages/libp2p-interfaces-compliance-tests/tsconfig.json new file mode 100644 index 000000000..bb74b1089 --- /dev/null +++ b/packages/libp2p-interfaces-compliance-tests/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../libp2p-connection" + }, + { + "path": "../libp2p-interfaces" + }, + { + "path": "../libp2p-pubsub" + }, + { + "path": "../libp2p-topology" + } + ] +} diff --git a/packages/interfaces/CHANGELOG.md b/packages/libp2p-interfaces/CHANGELOG.md similarity index 100% rename from packages/interfaces/CHANGELOG.md rename to packages/libp2p-interfaces/CHANGELOG.md diff --git a/packages/interfaces/README.md b/packages/libp2p-interfaces/README.md similarity index 100% rename from packages/interfaces/README.md rename to packages/libp2p-interfaces/README.md diff --git a/packages/libp2p-interfaces/package.json b/packages/libp2p-interfaces/package.json new file mode 100644 index 000000000..192040532 --- /dev/null +++ b/packages/libp2p-interfaces/package.json @@ -0,0 +1,114 @@ +{ + "name": "libp2p-interfaces", + "version": "1.2.0", + "description": "Interfaces for JS Libp2p", + "type": "module", + "files": [ + "src", + "dist" + ], + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "eslintConfig": { + "extends": "ipfs" + }, + "scripts": { + "lint": "aegir lint", + "dep-check": "aegir dep-check dist/src/**/*.js", + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-interfaces.git" + }, + "keywords": [ + "libp2p", + "interface" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-interfaces/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces#readme", + "dependencies": { + "multiaddr": "^10.0.0", + "multiformats": "^9.4.10" + }, + "devDependencies": { + "aegir": "^36.0.0" + }, + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "./connection": { + "import": "./dist/src/connection/index.js", + "types": "./dist/src/connection/index.d.ts" + }, + "./connection/status": { + "import": "./dist/src/connection/status.js", + "types": "./dist/src/connection/status.d.ts" + }, + "./content-routing": { + "import": "./dist/src/content-routing/index.js", + "types": "./dist/src/content-routing/index.d.ts" + }, + "./crypto": { + "import": "./dist/src/crypto/index.js", + "types": "./dist/src/crypto/index.d.ts" + }, + "./crypto/errors": { + "import": "./dist/src/crypto/errors.js", + "types": "./dist/src/crypto/errors.d.ts" + }, + "./keys": { + "import": "./dist/src/keys/index.js", + "types": "./dist/src/keys/index.d.ts" + }, + "./peer-discovery": { + "import": "./dist/src/peer-discovery/index.js", + "types": "./dist/src/peer-discovery/index.d.ts" + }, + "./peer-id": { + "import": "./dist/src/peer-id/index.js", + "types": "./dist/src/peer-id/index.d.ts" + }, + "./peer-routing": { + "import": "./dist/src/peer-routing/index.js", + "types": "./dist/src/peer-routing/index.d.ts" + }, + "./pubsub": { + "import": "./dist/src/pubsub/index.js", + "types": "./dist/src/pubsub/index.d.ts" + }, + "./record": { + "import": "./dist/src/record/index.js", + "types": "./dist/src/record/index.d.ts" + }, + "./stream-muxer": { + "import": "./dist/src/stream-muxer/index.js", + "types": "./dist/src/stream-muxer/index.d.ts" + }, + "./topology": { + "import": "./dist/src/topology/index.js", + "types": "./dist/src/topology/index.d.ts" + }, + "./transport": { + "import": "./dist/src/transport/index.js", + "types": "./dist/src/transport/index.d.ts" + }, + "./value-store": { + "import": "./dist/src/value-store/index.js", + "types": "./dist/src/value-store/index.d.ts" + } + } +} diff --git a/packages/libp2p-interfaces/src/connection/README.md b/packages/libp2p-interfaces/src/connection/README.md new file mode 100644 index 000000000..db1b3da59 --- /dev/null +++ b/packages/libp2p-interfaces/src/connection/README.md @@ -0,0 +1,256 @@ +interface-connection +================== + +This is a test suite and interface you can use to implement a connection. The connection interface contains all the metadata associated with it, as well as an array of the streams opened through this connection. In the same way as the connection, a stream contains properties with its metadata, plus an iterable duplex object that offers a mechanism for writing and reading data, with back pressure. This module and test suite were heavily inspired by abstract-blob-store and interface-stream-muxer. + +The primary goal of this module is to enable developers to pick, swap or upgrade their connection without losing the same API expectations and mechanisms such as back pressure and the ability to half close a connection. + +Publishing a test suite as a module lets multiple modules ensure compatibility since they use the same test suite. + +## Usage + +### Connection + +Before creating a connection from a transport compatible with `libp2p` it is important to understand some concepts: + +- **socket**: the underlying raw duplex connection between two nodes. It is created by the transports during a dial/listen. +- **[multiaddr connection](https://github.com/libp2p/interface-transport#multiaddrconnection)**: an abstraction over the socket to allow it to work with multiaddr addresses. It is a duplex connection that transports create to wrap the socket before passing to an upgrader that turns it into a standard connection (see below). +- **connection**: a connection between two _peers_ that has built in multiplexing and info about the connected peer. It is created from a [multiaddr connection](https://github.com/libp2p/interface-transport#multiaddrconnection) by an upgrader. The upgrader uses multistream-select to add secio and multiplexing and returns this object. +- **stream**: a muxed duplex channel of the `connection`. Each connection may have many streams. + +A connection stands for the libp2p communication duplex layer between two nodes. It is **not** the underlying raw transport duplex layer (socket), such as a TCP socket, but an abstracted layer that sits on top of the raw socket. + +This helps ensuring that the transport is responsible for socket management, while also allowing the application layer to handle the connection management. + +### Test suite + +```js +const tests = require('libp2p-interfaces-compliance-tests/connection') +describe('your connection', () => { + tests({ + // Options should be passed to your connection + async setup (options) { + return YourConnection + }, + async teardown () { + // cleanup resources created by setup() + } + }) +}) +``` + +## API + +### Connection + +A valid connection (one that follows this abstraction), must implement the following API: + +- type: `Connection` +```js +new Connection({ + localAddr, + remoteAddr, + localPeer, + remotePeer, + newStream, + close, + getStreams, + stat: { + direction, + timeline: { + open, + upgraded + }, + multiplexer, + encryption + } +}) +``` + - ` conn.localAddr` + - ` conn.remoteAddr` + - ` conn.localPeer` + - ` conn.remotePeer` + - ` conn.stat` + - ` conn.registry` + - `Array conn.streams` + - `Promise conn.newStream(Array)` + - ` conn.removeStream(id)` + - ` conn.addStream(stream, protocol, metadata)` + - `Promise<> conn.close()` + +It can be obtained as follows: + +```js +const { Connection } = require('interface-connection') + +const conn = new Connection({ + localAddr: maConn.localAddr, + remoteAddr: maConn.remoteAddr, + localPeer: this._peerId, + remotePeer, + newStream, + close: err => maConn.close(err), + getStreams, + stats: { + direction: 'outbound', + timeline: { + open: maConn.timeline.open, + upgraded: Date.now() + }, + multiplexer, + encryption + } +}) +``` + +#### Creating a connection instance + +- `JavaScript` - `const conn = new Connection({localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, direction, multiplexer, encryption})` + +Creates a new Connection instance. + +`localAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used by the local peer to reach the remote. +`remoteAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer. +`localPeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the local peer. +`remotePeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the remote peer. +`newStream` is the `function` responsible for getting a new muxed+multistream-selected stream. +`close` is the `function` responsible for closing the raw connection. +`getStreams` is the `function` responsible for getting the streams muxed within the connection. +`stats` is an `object` with the metadata of the connection. It contains: +- `direction` is a `string` indicating whether the connection is `inbound` or `outbound`. +- `timeline` is an `object` with the relevant events timestamps of the connection (`open`, `upgraded` and `closed`; the `closed` will be added when the connection is closed). +- `multiplexer` is a `string` with the connection multiplexing codec (optional). +- `encryption` is a `string` with the connection encryption method identifier (optional). +- `status` is a `string` indicating the overall status of the connection. It is one of [`'open'`, `'closing'`, `'closed'`] + +#### Create a new stream + +- `JavaScript` - `conn.newStream(protocols)` + +Create a new stream within the connection. + +`protocols` is an array of the intended protocol to use (by order of preference). Example: `[/echo/1.0.0]` + +It returns a `Promise` with an object with the following properties: + +```js +{ + stream, + protocol +} +``` + +The stream property contains the muxed stream, while the protocol contains the protocol codec used by the stream. + +#### Add stream metadata + +- `JavaScript` - `conn.addStream(stream, { protocol, ...metadata })` + +Add a new stream to the connection registry. + +`stream` is a muxed stream. +`protocol` is the string codec for the protocol used by the stream. Example: `/echo/1.0.0` +`metadata` is an object containing any additional, optional, stream metadata that you wish to track (such as its `tags`). + +#### Remove a from the registry + +- `JavaScript` - `conn.removeStream(id)` + +Removes the stream with the given id from the connection registry. + +`id` is the unique id of the stream for this connection. + + +#### Close connection + +- `JavaScript` - `conn.close()` + +This method closes the connection to the remote peer, as well as all the streams muxed within the connection. + +It returns a `Promise`. + +#### Connection identifier + +- `JavaScript` - `conn.id` + +This property contains the identifier of the connection. + +#### Connection streams registry + +- `JavaScript` - `conn.registry` + +This property contains a map with the muxed streams indexed by their id. This registry contains the protocol used by the stream, as well as its metadata. + +#### Remote peer + +- `JavaScript` - `conn.remotePeer` + +This property contains the remote `peer-id` of this connection. + +#### Local peer + +- `JavaScript` - `conn.localPeer` + +This property contains the local `peer-id` of this connection. + +#### Get the connection Streams + +- `JavaScript` - `conn.streams` + +This getter returns all the muxed streams within the connection. + +It returns an `Array`. + +#### Remote address + +- `JavaScript` - `conn.remoteAddr` + +This getter returns the `remote` [multiaddr](https://github.com/multiformats/multiaddr) address. + +#### Local address + +- `JavaScript` - `conn.localAddr` + +This getter returns the `local` [multiaddr](https://github.com/multiformats/multiaddr) address. + +#### Stat + +- `JavaScript` - `conn.stat` + +This getter returns an `Object` with the metadata of the connection, as follows: + +- `status`: + +This property contains the status of the connection. It can be either `open`, `closing` or `closed`. Once the connection is created it is in an `open` status. When a `conn.close()` happens, the status will change to `closing` and finally, after all the connection streams are properly closed, the status will be `closed`. These values can also be directly referenced by importing the `status` file: + +```js +const { + OPEN, CLOSING, CLOSED +} = require('libp2p-interfaces/src/connection/status') + +if (connection.stat.status === OPEN) { + // ... +} +``` + +- `timeline`: + +This property contains an object with the `open`, `upgraded` and `close` timestamps of the connection. Note that, the `close` timestamp is `undefined` until the connection is closed. + +- `direction`: + +This property contains the direction of the peer in the connection. It can be `inbound` or `outbound`. + +- `multiplexer`: + +This property contains the `multiplexing` codec being used in the connection. + +- `encryption`: + +This property contains the encryption method being used in the connection. It is `undefined` if the connection is not encrypted. + +#### Tags + +- `JavaScript` - `conn.tags` + +This property contains an array of tags associated with the connection. New tags can be pushed to this array during the connection's lifetime. diff --git a/packages/interfaces/src/connection/img/badge.png b/packages/libp2p-interfaces/src/connection/img/badge.png similarity index 100% rename from packages/interfaces/src/connection/img/badge.png rename to packages/libp2p-interfaces/src/connection/img/badge.png diff --git a/packages/interfaces/src/connection/img/badge.sketch b/packages/libp2p-interfaces/src/connection/img/badge.sketch similarity index 100% rename from packages/interfaces/src/connection/img/badge.sketch rename to packages/libp2p-interfaces/src/connection/img/badge.sketch diff --git a/packages/interfaces/src/connection/img/badge.svg b/packages/libp2p-interfaces/src/connection/img/badge.svg similarity index 100% rename from packages/interfaces/src/connection/img/badge.svg rename to packages/libp2p-interfaces/src/connection/img/badge.svg diff --git a/packages/libp2p-interfaces/src/connection/index.ts b/packages/libp2p-interfaces/src/connection/index.ts new file mode 100644 index 000000000..9bec82f78 --- /dev/null +++ b/packages/libp2p-interfaces/src/connection/index.ts @@ -0,0 +1,45 @@ +import type { Multiaddr } from 'multiaddr' +import type { PeerId } from '../peer-id' +import type { MuxedStream } from '../stream-muxer' +import type * as Status from './status.js' + +export interface Timeline { + open: number + upgraded?: number + close?: number +} + +export interface ConnectionStat { + direction: 'inbound' | 'outbound' + timeline: Timeline + multiplexer?: string + encryption?: string + status: keyof typeof Status +} + +export interface StreamData { + protocol: string + metadata: Record +} + +export interface Stream { + protocol: string + stream: MuxedStream +} + +export interface Connection { + id: string + stat: ConnectionStat + localAddr: Multiaddr + remoteAddr: Multiaddr + localPeer: PeerId + remotePeer: PeerId + registry: Map + tags: string[] + streams: MuxedStream[] + + newStream: (multicodecs: string[]) => Promise + addStream: (muxedStream: MuxedStream, streamData: StreamData) => void + removeStream: (id: string) => void + close: () => Promise +} diff --git a/packages/libp2p-interfaces/src/connection/status.ts b/packages/libp2p-interfaces/src/connection/status.ts new file mode 100644 index 000000000..a640d97e0 --- /dev/null +++ b/packages/libp2p-interfaces/src/connection/status.ts @@ -0,0 +1,4 @@ + +export const OPEN = 'OPEN' +export const CLOSING = 'CLOSING' +export const CLOSED = 'CLOSED' diff --git a/packages/interfaces/src/content-routing/README.md b/packages/libp2p-interfaces/src/content-routing/README.md similarity index 100% rename from packages/interfaces/src/content-routing/README.md rename to packages/libp2p-interfaces/src/content-routing/README.md diff --git a/packages/interfaces/src/content-routing/img/badge.png b/packages/libp2p-interfaces/src/content-routing/img/badge.png similarity index 100% rename from packages/interfaces/src/content-routing/img/badge.png rename to packages/libp2p-interfaces/src/content-routing/img/badge.png diff --git a/packages/interfaces/src/content-routing/img/badge.sketch b/packages/libp2p-interfaces/src/content-routing/img/badge.sketch similarity index 100% rename from packages/interfaces/src/content-routing/img/badge.sketch rename to packages/libp2p-interfaces/src/content-routing/img/badge.sketch diff --git a/packages/interfaces/src/content-routing/img/badge.svg b/packages/libp2p-interfaces/src/content-routing/img/badge.svg similarity index 100% rename from packages/interfaces/src/content-routing/img/badge.svg rename to packages/libp2p-interfaces/src/content-routing/img/badge.svg diff --git a/packages/libp2p-interfaces/src/content-routing/index.ts b/packages/libp2p-interfaces/src/content-routing/index.ts new file mode 100644 index 000000000..a29ce8503 --- /dev/null +++ b/packages/libp2p-interfaces/src/content-routing/index.ts @@ -0,0 +1,14 @@ +import type { PeerId } from '../peer-id' +import type { Multiaddr } from 'multiaddr' +import type { CID } from 'multiformats/cid' + +export interface ContentRoutingFactory { + new (options?: any): ContentRouting +} + +export interface ContentRouting { + provide: (cid: CID) => Promise + findProviders: (cid: CID, options: Object) => AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }> +} + +export default ContentRouting diff --git a/packages/interfaces/src/crypto/README.md b/packages/libp2p-interfaces/src/crypto/README.md similarity index 98% rename from packages/interfaces/src/crypto/README.md rename to packages/libp2p-interfaces/src/crypto/README.md index 8991f9d83..d4c3d089d 100644 --- a/packages/interfaces/src/crypto/README.md +++ b/packages/libp2p-interfaces/src/crypto/README.md @@ -23,7 +23,7 @@ interface-crypto You can also check out the [internal test suite](../../test/crypto/compliance.spec.js) to see the setup in action. ```js -const tests = require('libp2p-interfaces-compliance-tests/src/crypto') +const tests = require('libp2p-interfaces-compliance-tests/crypto') const yourCrypto = require('./your-crypto') tests({ diff --git a/packages/interfaces/src/crypto/errors.js b/packages/libp2p-interfaces/src/crypto/errors.ts similarity index 68% rename from packages/interfaces/src/crypto/errors.js rename to packages/libp2p-interfaces/src/crypto/errors.ts index 2c8f661cb..587809ea0 100644 --- a/packages/interfaces/src/crypto/errors.js +++ b/packages/libp2p-interfaces/src/crypto/errors.ts @@ -1,6 +1,7 @@ -'use strict' -class UnexpectedPeerError extends Error { +export class UnexpectedPeerError extends Error { + public code: string + constructor (message = 'Unexpected Peer') { super(message) this.code = UnexpectedPeerError.code @@ -11,7 +12,9 @@ class UnexpectedPeerError extends Error { } } -class InvalidCryptoExchangeError extends Error { +export class InvalidCryptoExchangeError extends Error { + public code: string + constructor (message = 'Invalid crypto exchange') { super(message) this.code = InvalidCryptoExchangeError.code @@ -22,7 +25,9 @@ class InvalidCryptoExchangeError extends Error { } } -class InvalidCryptoTransmissionError extends Error { +export class InvalidCryptoTransmissionError extends Error { + public code: string + constructor (message = 'Invalid crypto transmission') { super(message) this.code = InvalidCryptoTransmissionError.code @@ -32,9 +37,3 @@ class InvalidCryptoTransmissionError extends Error { return 'ERR_INVALID_CRYPTO_TRANSMISSION' } } - -module.exports = { - UnexpectedPeerError, - InvalidCryptoExchangeError, - InvalidCryptoTransmissionError -} diff --git a/packages/libp2p-interfaces/src/crypto/index.ts b/packages/libp2p-interfaces/src/crypto/index.ts new file mode 100644 index 000000000..0eea6a06a --- /dev/null +++ b/packages/libp2p-interfaces/src/crypto/index.ts @@ -0,0 +1,24 @@ +import type { PeerId } from '../peer-id' +import type { MultiaddrConnection } from '../transport' + +/** + * A libp2p crypto module must be compliant to this interface + * to ensure all exchanged data between two peers is encrypted. + */ +export interface Crypto { + protocol: string + /** + * Encrypt outgoing data to the remote party. + */ + secureOutbound: (localPeer: PeerId, connection: MultiaddrConnection, remotePeer: PeerId) => Promise + /** + * Decrypt incoming data. + */ + secureInbound: (localPeer: PeerId, connection: MultiaddrConnection, remotePeer?: PeerId) => Promise +} + +export interface SecureOutbound { + conn: MultiaddrConnection + remoteEarlyData: Uint8Array + remotePeer: PeerId +} diff --git a/packages/libp2p-interfaces/src/index.ts b/packages/libp2p-interfaces/src/index.ts new file mode 100644 index 000000000..6b9463918 --- /dev/null +++ b/packages/libp2p-interfaces/src/index.ts @@ -0,0 +1,5 @@ +export interface SelectFn { (key: Uint8Array, records: Uint8Array[]): number } +export interface ValidateFn { (a: Uint8Array, b: Uint8Array): Promise } + +export interface DhtSelectors { [key: string]: SelectFn } +export interface DhtValidators { [key: string]: { func: ValidateFn } } diff --git a/packages/interfaces/src/keys/README.md b/packages/libp2p-interfaces/src/keys/README.md similarity index 88% rename from packages/interfaces/src/keys/README.md rename to packages/libp2p-interfaces/src/keys/README.md index c158729d7..dfa14148f 100644 --- a/packages/interfaces/src/keys/README.md +++ b/packages/libp2p-interfaces/src/keys/README.md @@ -10,7 +10,7 @@ You can also check out the [internal test suite](../../test/crypto/compliance.spec.js) to see the setup in action. ```js -const tests = require('libp2p-interfaces-compliance-tests/src/keys') +const tests = require('libp2p-interfaces-compliance-tests/keys') const yourKeys = require('./your-keys') tests({ diff --git a/packages/interfaces/src/keys/types.d.ts b/packages/libp2p-interfaces/src/keys/index.ts similarity index 100% rename from packages/interfaces/src/keys/types.d.ts rename to packages/libp2p-interfaces/src/keys/index.ts diff --git a/packages/libp2p-interfaces/src/peer-data/index.ts b/packages/libp2p-interfaces/src/peer-data/index.ts new file mode 100644 index 000000000..a6579d4bb --- /dev/null +++ b/packages/libp2p-interfaces/src/peer-data/index.ts @@ -0,0 +1,8 @@ +import type { PeerId } from '../peer-id' +import type { Multiaddr } from 'multiaddr' + +export interface PeerData { + id: PeerId + multiaddrs: Multiaddr[] + protocols: string[] +} diff --git a/packages/interfaces/src/peer-discovery/README.md b/packages/libp2p-interfaces/src/peer-discovery/README.md similarity index 97% rename from packages/interfaces/src/peer-discovery/README.md rename to packages/libp2p-interfaces/src/peer-discovery/README.md index 6dd9fec85..dd683d4d7 100644 --- a/packages/interfaces/src/peer-discovery/README.md +++ b/packages/libp2p-interfaces/src/peer-discovery/README.md @@ -33,7 +33,7 @@ Include this badge in your readme if you make a new module that uses interface-p Install `interface-discovery` as one of the dependencies of your project and as a test file. Then, using `mocha` (for JavaScript) or a test runner with compatible API, do: ```js -const tests = require('libp2p-interfaces-compliance-tests/src/peer-discovery') +const tests = require('libp2p-interfaces-compliance-tests/peer-discovery') describe('your discovery', () => { // use all of the test suits diff --git a/packages/interfaces/src/peer-discovery/img/badge.png b/packages/libp2p-interfaces/src/peer-discovery/img/badge.png similarity index 100% rename from packages/interfaces/src/peer-discovery/img/badge.png rename to packages/libp2p-interfaces/src/peer-discovery/img/badge.png diff --git a/packages/interfaces/src/peer-discovery/img/badge.sketch b/packages/libp2p-interfaces/src/peer-discovery/img/badge.sketch similarity index 100% rename from packages/interfaces/src/peer-discovery/img/badge.sketch rename to packages/libp2p-interfaces/src/peer-discovery/img/badge.sketch diff --git a/packages/interfaces/src/peer-discovery/img/badge.svg b/packages/libp2p-interfaces/src/peer-discovery/img/badge.svg similarity index 100% rename from packages/interfaces/src/peer-discovery/img/badge.svg rename to packages/libp2p-interfaces/src/peer-discovery/img/badge.svg diff --git a/packages/libp2p-interfaces/src/peer-discovery/index.ts b/packages/libp2p-interfaces/src/peer-discovery/index.ts new file mode 100644 index 000000000..aae80e8e6 --- /dev/null +++ b/packages/libp2p-interfaces/src/peer-discovery/index.ts @@ -0,0 +1,18 @@ +import type { EventEmitter } from 'events' +import type { PeerData } from '../peer-data' + +export interface PeerDiscoveryFactory { + new (options?: any): PeerDiscovery + tag: string +} + +export interface PeerDiscoveryHandler { (peerData: PeerData): void } + +export interface PeerDiscovery extends EventEmitter { + start: () => void|Promise + stop: () => void|Promise + + on: (event: 'peer', handler: PeerDiscoveryHandler) => this +} + +export default PeerDiscovery diff --git a/packages/interfaces/src/peer-id/README.md b/packages/libp2p-interfaces/src/peer-id/README.md similarity index 95% rename from packages/interfaces/src/peer-id/README.md rename to packages/libp2p-interfaces/src/peer-id/README.md index f7a342387..49567facf 100644 --- a/packages/interfaces/src/peer-id/README.md +++ b/packages/libp2p-interfaces/src/peer-id/README.md @@ -28,7 +28,7 @@ Include this badge in your readme if you make a new module that uses interface-p Install `libp2p-interfaces-compliance-tests` as one of the development dependencies of your project and as a test file. Then, using `mocha` (for JavaScript) or a test runner with compatible API, do: ```js -const tests = require('libp2p-interfaces-compliance-tests/src/peer-id') +const tests = require('libp2p-interfaces-compliance-tests/peer-id') describe('your peer id', () => { // use all of the test suits diff --git a/packages/interfaces/src/peer-id/types.d.ts b/packages/libp2p-interfaces/src/peer-id/index.ts similarity index 57% rename from packages/interfaces/src/peer-id/types.d.ts rename to packages/libp2p-interfaces/src/peer-id/index.ts index d469c0eb4..ad6bc6f4c 100644 --- a/packages/interfaces/src/peer-id/types.d.ts +++ b/packages/libp2p-interfaces/src/peer-id/index.ts @@ -1,137 +1,137 @@ import type { CID } from 'multiformats/cid' -import type { PublicKey, PrivateKey, KeyType } from '../keys/types' +import type { PublicKey, PrivateKey, KeyType } from '../keys' interface PeerIdJSON { - readonly id: string; - readonly pubKey?: string; - readonly privKey?: string; + readonly id: string + readonly pubKey?: string + readonly privKey?: string } interface CreateOptions { - bits?: number; - keyType?: KeyType; + bits?: number + keyType?: KeyType } export interface PeerId { - readonly id: Uint8Array; - privKey: PrivateKey | undefined; - pubKey: PublicKey | undefined; + readonly id: Uint8Array + privKey: PrivateKey + pubKey: PublicKey /** * Return the protobuf version of the public key, matching go ipfs formatting */ - marshalPubKey ():Uint8Array | undefined; + marshalPubKey: () => Uint8Array /** * Return the protobuf version of the private key, matching go ipfs formatting */ - marshalPrivKey (): Uint8Array | undefined; + marshalPrivKey: () => Uint8Array /** * Return the protobuf version of the peer-id */ - marshal (excludePriv?: boolean): Uint8Array; + marshal: (excludePriv?: boolean) => Uint8Array /** * String representation */ - toPrint (): string; + toPrint: () => string /** * The jsonified version of the key, matching the formatting of go-ipfs for its config file */ - toJSON (): PeerIdJSON; + toJSON: () => PeerIdJSON /** * Encode to hex. */ - toHexString ():string; + toHexString: () => string /** * Return raw id bytes */ - toBytes () : Uint8Array; + toBytes: () => Uint8Array /** * Encode to base58 string. */ - toB58String (): string; + toB58String: () => string /** * Self-describing String representation * in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 */ - toString ():string; + toString: () => string /** * Checks the equality of `this` peer against a given PeerId. */ - equals (id: Uint8Array|PeerId): boolean | never; + equals: (id: any) => boolean /** * Check if this PeerId instance is valid (privKey -> pubKey -> Id) */ - isValid (): boolean; + isValid: () => boolean /** * Check if the PeerId has an inline public key. */ - hasInlinePublicKey (): boolean; + hasInlinePublicKey: () => boolean } export interface PeerIdFactory { /** * Create a new PeerId. **/ - create (args: CreateOptions): Promise; + create: (args?: CreateOptions) => Promise /** * Create PeerId from raw bytes. */ - createFromBytes (buf: Uint8Array): PeerId; + createFromBytes: (buf: Uint8Array) => PeerId /** * Create PeerId from base58-encoded string. */ - createFromB58String (str: string): PeerId; + createFromB58String: (str: string) => PeerId /** * Create PeerId from hex string. */ - createFromHexString (str: string): PeerId; + createFromHexString: (str: string) => PeerId /** * Create PeerId from CID. */ - createFromCID (cid: CID | Uint8Array | string): PeerId + createFromCID: (cid: CID | Uint8Array | string) => PeerId /** * Create PeerId from public key. */ - createFromPubKey (key: Uint8Array | string): Promise; + createFromPubKey: (key: Uint8Array | string) => Promise /** * Create PeerId from private key. */ - createFromPrivKey (key: Uint8Array | string): Promise; + createFromPrivKey: (key: Uint8Array | string) => Promise /** * Create PeerId from PeerId JSON formatted object. */ - createFromJSON (obj: PeerIdJSON): Promise; + createFromJSON: (obj: PeerIdJSON) => Promise /** * Create PeerId from Protobuf bytes. */ - createFromProtobuf (buf: Uint8Array | string): Promise; + createFromProtobuf: (buf: Uint8Array | string) => Promise /** * Parse PeerId from string, maybe base58btc encoded without multibase prefix */ - parse (str: string): PeerId + parse: (str: string) => PeerId /** * Checks if a value is an instance of PeerId. */ - isPeerId (peerId:unknown): boolean; + isPeerId: (peerId: unknown) => boolean } diff --git a/packages/interfaces/src/peer-routing/README.md b/packages/libp2p-interfaces/src/peer-routing/README.md similarity index 100% rename from packages/interfaces/src/peer-routing/README.md rename to packages/libp2p-interfaces/src/peer-routing/README.md diff --git a/packages/interfaces/src/peer-routing/img/badge.png b/packages/libp2p-interfaces/src/peer-routing/img/badge.png similarity index 100% rename from packages/interfaces/src/peer-routing/img/badge.png rename to packages/libp2p-interfaces/src/peer-routing/img/badge.png diff --git a/packages/interfaces/src/peer-routing/img/badge.sketch b/packages/libp2p-interfaces/src/peer-routing/img/badge.sketch similarity index 100% rename from packages/interfaces/src/peer-routing/img/badge.sketch rename to packages/libp2p-interfaces/src/peer-routing/img/badge.sketch diff --git a/packages/interfaces/src/peer-routing/img/badge.svg b/packages/libp2p-interfaces/src/peer-routing/img/badge.svg similarity index 100% rename from packages/interfaces/src/peer-routing/img/badge.svg rename to packages/libp2p-interfaces/src/peer-routing/img/badge.svg diff --git a/packages/libp2p-interfaces/src/peer-routing/index.ts b/packages/libp2p-interfaces/src/peer-routing/index.ts new file mode 100644 index 000000000..a7ec585eb --- /dev/null +++ b/packages/libp2p-interfaces/src/peer-routing/index.ts @@ -0,0 +1,13 @@ +import type { PeerId } from '../peer-id' +import type { Multiaddr } from 'multiaddr' + +export interface PeerRoutingFactory { + new (options?: any): PeerRouting +} + +export interface PeerRouting { + findPeer: (peerId: PeerId, options?: Object) => Promise<{ id: PeerId, multiaddrs: Multiaddr[] }> + getClosestPeers: (key: Uint8Array, options?: Object) => AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }> +} + +export default PeerRouting diff --git a/packages/interfaces/src/pubsub/README.md b/packages/libp2p-interfaces/src/pubsub/README.md similarity index 99% rename from packages/interfaces/src/pubsub/README.md rename to packages/libp2p-interfaces/src/pubsub/README.md index 1ad9e6a5b..b9f44f75b 100644 --- a/packages/interfaces/src/pubsub/README.md +++ b/packages/libp2p-interfaces/src/pubsub/README.md @@ -225,7 +225,7 @@ Validates a message according to the signature policy and topic-specific validat ```js 'use strict' -const tests = require('libp2p-interfaces-compliance-tests/src/pubsub') +const tests = require('libp2p-interfaces-compliance-tests/pubsub') const YourPubsubRouter = require('../src') describe('compliance', () => { diff --git a/packages/libp2p-interfaces/src/pubsub/index.ts b/packages/libp2p-interfaces/src/pubsub/index.ts new file mode 100644 index 000000000..c644863e8 --- /dev/null +++ b/packages/libp2p-interfaces/src/pubsub/index.ts @@ -0,0 +1,88 @@ +import type { EventEmitter } from 'events' +import type { PeerId } from '../peer-id' +import type { Pushable } from 'it-pushable' +import type { Registrar } from '../registrar' + +/** + * On the producing side: + * * Build messages with the signature, key (from may be enough for certain inlineable public key types), from and seqno fields. + * + * On the consuming side: + * * Enforce the fields to be present, reject otherwise. + * * Propagate only if the fields are valid and signature can be verified, reject otherwise. + */ +export type StrictSign = 'StrictSign' + +/** + * On the producing side: + * * Build messages without the signature, key, from and seqno fields. + * * The corresponding protobuf key-value pairs are absent from the marshalled message, not just empty. + * + * On the consuming side: + * * Enforce the fields to be absent, reject otherwise. + * * Propagate only if the fields are absent, reject otherwise. + * * A message_id function will not be able to use the above fields, and should instead rely on the data field. A commonplace strategy is to calculate a hash. + */ +export type StrictNoSign = 'StrictNoSign' + +export interface Message { + from?: Uint8Array + receivedFrom: string + topicIDs: string[] + seqno?: Uint8Array + data: Uint8Array + signature?: Uint8Array + key?: Uint8Array +} + +export interface PeerStreams extends EventEmitter { + id: PeerId + protocol: string + outboundStream: Pushable | undefined + inboundStream: AsyncIterable | undefined +} + +export interface PubsubOptions { + debugName?: string + multicodecs: string[] + libp2p: any + + /** + * defines how signatures should be handled + */ + globalSignaturePolicy?: StrictSign | StrictNoSign + + /** + * if can relay messages not subscribed + */ + canRelayMessage?: boolean + + /** + * if publish should emit to self, if subscribed + */ + emitSelf?: boolean + + /** + * handle this many incoming pubsub messages concurrently + */ + messageProcessingConcurrency?: number +} + +export interface PubSub extends EventEmitter { + peerId: PeerId + started: boolean + peers: Map + subscriptions: Set + topics: Map> + globalSignaturePolicy: StrictSign | StrictNoSign + registrar: Registrar + + start: () => void + stop: () => void + getTopics: () => string[] + subscribe: (topic: string) => void + getSubscribers: (topic: string) => string[] + unsubscribe: (topic: string) => void + publish: (topic: string, data: Uint8Array) => Promise + validate: (message: Message) => Promise +} diff --git a/packages/interfaces/src/record/README.md b/packages/libp2p-interfaces/src/record/README.md similarity index 97% rename from packages/interfaces/src/record/README.md rename to packages/libp2p-interfaces/src/record/README.md index f311eb557..a34fbc6d9 100644 --- a/packages/interfaces/src/record/README.md +++ b/packages/libp2p-interfaces/src/record/README.md @@ -12,7 +12,7 @@ A record can also contain a Uint8Array codec (ideally registered as a [multicode ## Usage ```js -const tests = require('libp2p-interfaces-compliance-tests/src/record') +const tests = require('libp2p-interfaces-compliance-tests/record') describe('your record', () => { tests({ async setup () { diff --git a/packages/interfaces/src/record/types.d.ts b/packages/libp2p-interfaces/src/record/index.ts similarity index 78% rename from packages/interfaces/src/record/types.d.ts rename to packages/libp2p-interfaces/src/record/index.ts index 97212a4fd..38ad3e509 100644 --- a/packages/interfaces/src/record/types.d.ts +++ b/packages/libp2p-interfaces/src/record/index.ts @@ -5,17 +5,17 @@ export interface Record { /** * signature domain. */ - domain: string; + domain: string /** * identifier of the type of record */ - codec: Uint8Array; + codec: Uint8Array /** * Marshal a record to be used in an envelope. */ - marshal(): Uint8Array; + marshal: () => Uint8Array /** * Verifies if the other provided Record is identical to this one. */ - equals(other: unknown): boolean + equals: (other: unknown) => boolean } diff --git a/packages/libp2p-interfaces/src/registrar/index.ts b/packages/libp2p-interfaces/src/registrar/index.ts new file mode 100644 index 000000000..5ce8ffd4f --- /dev/null +++ b/packages/libp2p-interfaces/src/registrar/index.ts @@ -0,0 +1,38 @@ +import type { Connection } from '../connection' +import type { MuxedStream } from '../stream-muxer' +import type { PeerId } from '../peer-id' +import type { PeerData } from '../peer-data' + +export interface IncomingStreamEvent { + protocol: string + stream: MuxedStream + connection: Connection +} + +export interface ChangeProtocolsEvent { + peerId: PeerId + protocols: string[] +} + +export interface ProtoBook { + get: (peerId: PeerId) => string[] +} + +export interface PeerStore { + on: (event: 'change:protocols', handler: (event: ChangeProtocolsEvent) => void) => void + protoBook: ProtoBook + peers: Map + get: (peerId: PeerId) => PeerData +} + +export interface Registrar { + handle: (multicodecs: string[], handler: (event: IncomingStreamEvent) => void) => void + register: (topology: any) => string + unregister: (id: string) => void + getConnection: (peerId: PeerId) => Connection | undefined + peerStore: PeerStore + + connectionManager: { + on: (event: 'peer:connect', handler: (connection: Connection) => void) => void + } +} diff --git a/packages/interfaces/src/stream-muxer/README.md b/packages/libp2p-interfaces/src/stream-muxer/README.md similarity index 97% rename from packages/interfaces/src/stream-muxer/README.md rename to packages/libp2p-interfaces/src/stream-muxer/README.md index d3c944c6e..a7e247229 100644 --- a/packages/interfaces/src/stream-muxer/README.md +++ b/packages/libp2p-interfaces/src/stream-muxer/README.md @@ -26,7 +26,7 @@ Include this badge in your readme if you make a new module that uses interface-s Install `interface-stream-muxer` as one of the dependencies of your project and as a test file. Then, using `mocha` (for JavaScript) or a test runner with compatible API, do: ```js -const test = require('libp2p-interfaces-compliance-tests/src/stream-muxer') +const test = require('libp2p-interfaces-compliance-tests/stream-muxer') const common = { async setup () { @@ -53,7 +53,7 @@ e.g. ```js const Muxer = require('your-muxer-module') -const pipe = require('it-pipe') +import pipe from 'it-pipe' // Create a duplex muxer const muxer = new Muxer() diff --git a/packages/interfaces/src/stream-muxer/img/badge.png b/packages/libp2p-interfaces/src/stream-muxer/img/badge.png similarity index 100% rename from packages/interfaces/src/stream-muxer/img/badge.png rename to packages/libp2p-interfaces/src/stream-muxer/img/badge.png diff --git a/packages/interfaces/src/stream-muxer/img/badge.sketch b/packages/libp2p-interfaces/src/stream-muxer/img/badge.sketch similarity index 100% rename from packages/interfaces/src/stream-muxer/img/badge.sketch rename to packages/libp2p-interfaces/src/stream-muxer/img/badge.sketch diff --git a/packages/interfaces/src/stream-muxer/img/badge.svg b/packages/libp2p-interfaces/src/stream-muxer/img/badge.svg similarity index 100% rename from packages/interfaces/src/stream-muxer/img/badge.svg rename to packages/libp2p-interfaces/src/stream-muxer/img/badge.svg diff --git a/packages/libp2p-interfaces/src/stream-muxer/index.ts b/packages/libp2p-interfaces/src/stream-muxer/index.ts new file mode 100644 index 000000000..285eca0fe --- /dev/null +++ b/packages/libp2p-interfaces/src/stream-muxer/index.ts @@ -0,0 +1,48 @@ + +export interface MuxerFactory { + new (options: MuxerOptions): Muxer + multicodec: string +} + +/** + * A libp2p stream muxer + */ +export interface Muxer { + readonly streams: MuxedStream[] + /** + * Initiate a new stream with the given name. If no name is + * provided, the id of th stream will be used. + */ + newStream: (name?: string) => MuxedStream + + /** + * A function called when receiving a new stream from the remote. + */ + onStream: (stream: MuxedStream) => void + + /** + * A function called when a stream ends. + */ + onStreamEnd: (stream: MuxedStream) => void +} + +export interface MuxerOptions { + onStream?: (stream: MuxedStream) => void + onStreamEnd?: (stream: MuxedStream) => void + maxMsgSize?: number +} + +export interface MuxedTimeline { + open: number + close?: number +} + +export interface MuxedStream extends AsyncIterable { + close: () => void + abort: () => void + reset: () => void + sink: (source: AsyncIterable | Iterable) => Promise + source: AsyncIterable | Iterable + timeline: MuxedTimeline + id: string +} diff --git a/packages/interfaces/src/topology/README.md b/packages/libp2p-interfaces/src/topology/README.md similarity index 100% rename from packages/interfaces/src/topology/README.md rename to packages/libp2p-interfaces/src/topology/README.md diff --git a/packages/libp2p-interfaces/src/topology/index.ts b/packages/libp2p-interfaces/src/topology/index.ts new file mode 100644 index 000000000..af0bbca33 --- /dev/null +++ b/packages/libp2p-interfaces/src/topology/index.ts @@ -0,0 +1,39 @@ +import type { PeerId } from '../peer-id' +import type { Connection } from '../connection' + +export interface onConnectHandler { (peerId: PeerId, conn: Connection): void } +export interface onDisconnectHandler { (peerId: PeerId, conn?: Connection): void } + +export interface Handlers { + onConnect?: onConnectHandler + onDisconnect?: onDisconnectHandler +} + +export interface TopologyOptions { + /** + * minimum needed connections + */ + min?: number + + /** + * maximum needed connections + */ + max?: number + handlers: Handlers +} + +export interface Topology { + min: number + max: number + peers: Set + + disconnect: (id: PeerId) => void +} + +export interface MulticodecTopologyOptions extends TopologyOptions { + multicodecs: string[] +} + +export interface MulticodecTopology extends Topology { + multicodecs: string[] +} diff --git a/packages/interfaces/src/transport/README.md b/packages/libp2p-interfaces/src/transport/README.md similarity index 98% rename from packages/interfaces/src/transport/README.md rename to packages/libp2p-interfaces/src/transport/README.md index 0ffe12bc2..420a7278e 100644 --- a/packages/interfaces/src/transport/README.md +++ b/packages/libp2p-interfaces/src/transport/README.md @@ -33,7 +33,7 @@ Include this badge in your readme if you make a module that is compatible with t /* eslint-env mocha */ 'use strict' -const tests = require('libp2p-interfaces-compliance-tests/src/transport') +const tests = require('libp2p-interfaces-compliance-tests/transport') const multiaddr = require('multiaddr') const YourTransport = require('../src') @@ -150,7 +150,7 @@ const controller = new AbortController() try { const conn = await mytransport.dial(ma, { signal: controller.signal }) // Do stuff with conn here ... -} catch (err) { +} catch (err: any) { if(err.code === AbortError.code) { // Dial was aborted, just bail out return diff --git a/packages/interfaces/src/transport/errors.js b/packages/libp2p-interfaces/src/transport/errors.ts similarity index 69% rename from packages/interfaces/src/transport/errors.js rename to packages/libp2p-interfaces/src/transport/errors.ts index 38609c1ab..999045cd7 100644 --- a/packages/interfaces/src/transport/errors.js +++ b/packages/libp2p-interfaces/src/transport/errors.ts @@ -1,6 +1,8 @@ -'use strict' -class AbortError extends Error { +export class AbortError extends Error { + public readonly code: string + public readonly type: string + constructor () { super('The operation was aborted') this.code = AbortError.code @@ -15,7 +17,3 @@ class AbortError extends Error { return 'aborted' } } - -module.exports = { - AbortError -} diff --git a/packages/interfaces/src/transport/img/badge.png b/packages/libp2p-interfaces/src/transport/img/badge.png similarity index 100% rename from packages/interfaces/src/transport/img/badge.png rename to packages/libp2p-interfaces/src/transport/img/badge.png diff --git a/packages/interfaces/src/transport/img/badge.sketch b/packages/libp2p-interfaces/src/transport/img/badge.sketch similarity index 100% rename from packages/interfaces/src/transport/img/badge.sketch rename to packages/libp2p-interfaces/src/transport/img/badge.sketch diff --git a/packages/interfaces/src/transport/img/badge.svg b/packages/libp2p-interfaces/src/transport/img/badge.svg similarity index 100% rename from packages/interfaces/src/transport/img/badge.svg rename to packages/libp2p-interfaces/src/transport/img/badge.svg diff --git a/packages/libp2p-interfaces/src/transport/index.ts b/packages/libp2p-interfaces/src/transport/index.ts new file mode 100644 index 000000000..a085c4e44 --- /dev/null +++ b/packages/libp2p-interfaces/src/transport/index.ts @@ -0,0 +1,74 @@ +import type events from 'events' +import type { Multiaddr } from 'multiaddr' +import type { Connection } from '../connection' + +export interface AbortOptions { + signal?: AbortSignal +} + +export interface TransportFactory { + new(upgrader: Upgrader): Transport +} + +/** + * A libp2p transport is understood as something that offers a dial and listen interface to establish connections. + */ +export interface Transport { + /** + * Dial a given multiaddr. + */ + dial: (ma: Multiaddr, options?: DialOptions) => Promise + /** + * Create transport listeners. + */ + createListener: (options: ListenerOptions, handler?: (connection: Connection) => void) => Listener + /** + * Takes a list of `Multiaddr`s and returns only valid addresses for the transport + */ + filter: (multiaddrs: Multiaddr[]) => Multiaddr[] +} + +export interface Listener extends events.EventEmitter { + /** + * Start a listener + */ + listen: (multiaddr: Multiaddr) => Promise + /** + * Get listen addresses + */ + getAddrs: () => Multiaddr[] + /** + * Close listener + * + * @returns {Promise} + */ + close: () => Promise +} + +export interface Upgrader { + /** + * Upgrades an outbound connection on `transport.dial`. + */ + upgradeOutbound: (maConn: MultiaddrConnection) => Promise + + /** + * Upgrades an inbound connection on transport listener. + */ + upgradeInbound: (maConn: MultiaddrConnection) => Promise +} + +export interface MultiaddrConnectionTimeline { + open: number + upgraded?: number + close?: number +} + +export interface MultiaddrConnection { + sink: (source: AsyncIterable | Iterable) => Promise + source: AsyncIterable | Iterable + close: (err?: Error) => Promise + conn: unknown + remoteAddr: Multiaddr + localAddr?: Multiaddr + timeline: MultiaddrConnectionTimeline +} diff --git a/packages/interfaces/src/value-store/README.md b/packages/libp2p-interfaces/src/value-store/README.md similarity index 100% rename from packages/interfaces/src/value-store/README.md rename to packages/libp2p-interfaces/src/value-store/README.md diff --git a/packages/libp2p-interfaces/src/value-store/index.ts b/packages/libp2p-interfaces/src/value-store/index.ts new file mode 100644 index 000000000..aa1fae22c --- /dev/null +++ b/packages/libp2p-interfaces/src/value-store/index.ts @@ -0,0 +1,13 @@ +import type { PeerId } from '../peer-id' + +export interface GetValueResult { + from: PeerId + val: Uint8Array +} + +export interface ValueStore { + put: (key: Uint8Array, value: Uint8Array, options?: Object) => Promise + get: (key: Uint8Array, options?: Object) => Promise +} + +export default ValueStore diff --git a/packages/libp2p-interfaces/tsconfig.json b/packages/libp2p-interfaces/tsconfig.json new file mode 100644 index 000000000..5b4acb638 --- /dev/null +++ b/packages/libp2p-interfaces/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src" + ] +} diff --git a/packages/libp2p-pubsub/package.json b/packages/libp2p-pubsub/package.json new file mode 100644 index 000000000..6eeb48ffb --- /dev/null +++ b/packages/libp2p-pubsub/package.json @@ -0,0 +1,106 @@ +{ + "name": "libp2p-pubsub", + "version": "0.6.0", + "description": "libp2p pubsub base class", + "type": "module", + "files": [ + "src", + "dist" + ], + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "eslintConfig": { + "extends": "ipfs", + "ignorePatterns": [ + "src/message/*.d.ts", + "src/message/*.js" + ] + }, + "scripts": { + "lint": "aegir lint", + "dep-check": "aegir dep-check dist/src/**/*.js", + "build": "npm run build:types && npm run build:copy-proto-files", + "build:types": "tsc", + "build:proto": "npm run build:proto:rpc && npm run build:proto:topic-descriptor", + "build:proto:rpc": "pbjs -t static-module -w es6 -r libp2p-pubsub-rpc --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/message/rpc.js ./src/message/rpc.proto", + "build:proto:topic-descriptor": "pbjs -t static-module -w es6 -r libp2p-pubsub-topic-descriptor --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/message/topic-descriptor.js ./src/message/topic-descriptor.proto", + "build:proto-types": "npm run build:proto-types:rpc && npm run build:proto-types:topic-descriptor", + "build:proto-types:rpc": "pbts -o src/message/rpc.d.ts src/message/rpc.js", + "build:proto-types:topic-descriptor": "pbts -o src/message/topic-descriptor.d.ts src/message/topic-descriptor.js", + "build:copy-proto-files": "cp src/message/*.js dist/src/message && cp src/message/*.d.ts dist/src/message", + "pretest": "npm run build", + "test": "aegir test -f ./dist/test/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-interfaces.git" + }, + "keywords": [ + "libp2p", + "interface" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-interfaces/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-pubsub#readme#readme", + "dependencies": { + "debug": "^4.3.2", + "err-code": "^3.0.1", + "iso-random-stream": "^2.0.0", + "it-length-prefixed": "^5.0.3", + "it-pipe": "^1.1.0", + "libp2p-interfaces": "^1.2.0", + "libp2p-topology": "^0.0.1", + "multiaddr": "^10.0.1", + "multiformats": "^9.4.10", + "p-queue": "^7.1.0", + "peer-id": "^0.15.3", + "uint8arrays": "^3.0.0" + }, + "devDependencies": { + "@types/debug": "^4.1.7", + "@types/bl": "^5.0.2", + "aegir": "^36.0.0", + "protobufjs": "^6.10.2", + "util": "^0.12.4" + }, + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "./errors": { + "import": "./dist/src/errors.js", + "types": "./dist/src/errors.d.ts" + }, + "./peer-streams": { + "import": "./dist/src/peer-streams.js", + "types": "./dist/src/peer-streams.d.ts" + }, + "./signature-policy": { + "import": "./dist/src/signature-policy.js", + "types": "./dist/src/signature-policy.d.ts" + }, + "./utils": { + "import": "./dist/src/utils.js", + "types": "./dist/src/utils.d.ts" + }, + "./message/rpc": { + "import": "./dist/src/message/rpc.js", + "types": "./dist/src/message/rpc.d.ts" + }, + "./message/topic-descriptor": { + "import": "./dist/src/message/topic-descriptor.js", + "types": "./dist/src/message/topic-descriptor.d.ts" + } + } +} diff --git a/packages/libp2p-pubsub/src/README.md b/packages/libp2p-pubsub/src/README.md new file mode 100644 index 000000000..016b75d10 --- /dev/null +++ b/packages/libp2p-pubsub/src/README.md @@ -0,0 +1,251 @@ +# libp2p-pubsub + +This module contains a base implementation for a libp2p pubsub router implementation. It should be extended to implement a pubsub router compatible with libp2p. + +## Table of Contents + +- [Implementations using this interface](#implementations-using-this-interface) +- [Interface usage](#interface-usage) + - [Extend interface](#extend-interface) + - [Example](#example) +- [API](#api) + - [Constructor](#constructor) + - [`new Pubsub({options})`](#new-pubsuboptions) + - [Parameters](#parameters) + - [Start](#start) + - [`pubsub.start()`](#pubsubstart) + - [Stop](#stop) + - [`pubsub.stop()`](#pubsubstop) + - [Publish](#publish) + - [`pubsub.publish(topic, message)`](#pubsubpublishtopic-message) + - [Parameters](#parameters-1) + - [Returns](#returns) + - [Subscribe](#subscribe) + - [`pubsub.subscribe(topic)`](#pubsubsubscribetopic) + - [Parameters](#parameters-2) + - [Unsubscribe](#unsubscribe) + - [`pubsub.unsubscribe(topic)`](#pubsubunsubscribetopic) + - [Parameters](#parameters-3) + - [Get Topics](#get-topics) + - [`pubsub.getTopics()`](#pubsubgettopics) + - [Returns](#returns-1) + - [Get Peers Subscribed to a topic](#get-peers-subscribed-to-a-topic) + - [`pubsub.getSubscribers(topic)`](#pubsubgetsubscriberstopic) + - [Parameters](#parameters-4) + - [Returns](#returns-2) + - [Validate](#validate) + - [`pubsub.validate(message)`](#pubsubvalidatemessage) + - [Parameters](#parameters-5) + - [Returns](#returns-3) +- [Test suite usage](#test-suite-usage) + +## Implementations using this interface + +You can check the following implementations as examples for building your own pubsub router. + +- [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub) +- [ChainSafe/js-libp2p-gossipsub](https://github.com/ChainSafe/js-libp2p-gossipsub) + +## Interface usage + +`interface-pubsub` abstracts the implementation protocol registration within `libp2p` and takes care of all the protocol connections and streams, as well as the subscription management and the features describe in the libp2p [pubsub specs](https://github.com/libp2p/specs/tree/master/pubsub). This way, a pubsub implementation can focus on its message routing algorithm, instead of also needing to create the setup for it. + +### Extend interface + +A pubsub router implementation should start by extending the `interface-pubsub` class and **MUST** override the `_publish` function, according to the router algorithms. This function is responsible for forwarding publish messages to other peers, as well as forwarding received messages if the router provides the `canRelayMessage` option to the base implementation. + +Other functions, such as `start`, `stop`, `subscribe`, `unsubscribe`, `_encodeRpc`, `_decodeRpc`, `_processRpcMessage`, `_addPeer` and `_removePeer` may be overwritten if the pubsub implementation needs to customize their logic. Implementations overriding these functions **MUST** call `super`. + +The `start` and `stop` functions are responsible for the registration of the pubsub protocol with libp2p. The `stop` function also guarantees that the open streams in the protocol are properly closed. + +The `subscribe` and `unsubscribe` functions take care of the subscription management and its inherent message propagation. + +When using a custom protobuf definition for message marshalling, you should override `_encodeRpc` and `_decodeRpc` to use the new protobuf instead of the default one. + +`_processRpcMessage` is responsible for handling messages received from other peers. This should be extended if further operations/validations are needed by the router. + +The `_addPeer` and `_removePeer` functions are called when new peers running the pubsub router protocol establish a connection with the peer. They are used for tracking the open streams between the peers. + +All the remaining functions **MUST NOT** be overwritten. + +### Example + +The following example aims to show how to create your pubsub implementation extending this base protocol. The pubsub implementation will handle the subscriptions logic. + +```JavaScript +const Pubsub = require('libp2p-interfaces/src/pubsub') + +class PubsubImplementation extends Pubsub { + constructor({ libp2p, options }) + super({ + debugName: 'libp2p:pubsub', + multicodecs: '/pubsub-implementation/1.0.0', + libp2p, + globalSigningPolicy: options.globalSigningPolicy + }) + } + + _publish (message) { + // Required to be implemented by the subclass + // Routing logic for the message + } +} +``` + +## API + +The interface aims to specify a common interface that all pubsub router implementation should follow. A pubsub router implementation should extend the [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). When peers receive pubsub messages, these messages will be emitted by the event emitter where the `eventName` will be the `topic` associated with the message. + +### Constructor + +The base class constructor configures the pubsub instance for use with a libp2p instance. It includes settings for logging, signature policies, etc. + +#### `new Pubsub({options})` + +##### Parameters + +| Name | Type | Description | Default | +|------|------|-------------|---------| +| options.libp2p | `Libp2p` | libp2p instance | required, no default | +| options.debugName | `string` | log namespace | required, no default | +| options.multicodecs | `string \| Array` | protocol identifier(s) | required, no default | +| options.globalSignaturePolicy | `'StrictSign' \| 'StrictNoSign'` | signature policy to be globally applied | `'StrictSign'` | +| options.canRelayMessage | `boolean` | if can relay messages if not subscribed | `false` | +| options.emitSelf | `boolean` | if `publish` should emit to self, if subscribed | `false` | + +### Start + +Starts the pubsub subsystem. The protocol will be registered to `libp2p`, which will result in pubsub being notified when peers who support the protocol connect/disconnect to `libp2p`. + +#### `pubsub.start()` + +### Stop + +Stops the pubsub subsystem. The protocol will be unregistered from `libp2p`, which will remove all listeners for the protocol and the established connections will be closed. + +#### `pubsub.stop()` + +### Publish + +Publish data message to pubsub topics. + +#### `pubsub.publish(topic, message)` + +##### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | pubsub topic | +| message | `Uint8Array` | message to publish | + +##### Returns + +| Type | Description | +|------|-------------| +| `Promise` | resolves once the message is published to the network | + +### Subscribe + +Subscribe to the given topic. + +#### `pubsub.subscribe(topic)` + +##### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | pubsub topic | + +### Unsubscribe + +Unsubscribe from the given topic. + +#### `pubsub.unsubscribe(topic)` + +##### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | pubsub topic | + +### Get Topics + +Get the list of topics which the peer is subscribed to. + +#### `pubsub.getTopics()` + +##### Returns + +| Type | Description | +|------|-------------| +| `Array` | Array of subscribed topics | + +### Get Peers Subscribed to a topic + +Get a list of the [PeerId](https://github.com/libp2p/js-peer-id) strings that are subscribed to one topic. + +#### `pubsub.getSubscribers(topic)` + +##### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | pubsub topic | + +##### Returns + +| Type | Description | +|------|-------------| +| `Array` | Array of base-58 PeerId's | + +### Validate + +Validates a message according to the signature policy and topic-specific validation function. + +#### `pubsub.validate(message)` + +##### Parameters + +| Name | Type | Description | +|------|------|-------------| +| message | `Message` | a pubsub message | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | resolves if the message is valid | + +## Test suite usage + +```js +'use strict' + +const tests = require('libp2p-interfaces-compliance-tests/pubsub') +const YourPubsubRouter = require('../src') + +describe('compliance', () => { + let peers + let pubsubNodes = [] + + tests({ + async setup (number = 1, options = {}) { + // Create number pubsub nodes with libp2p + peers = await createPeers({ number }) + + peers.forEach((peer) => { + const ps = new YourPubsubRouter(peer, options) + + pubsubNodes.push(ps) + }) + + return pubsubNodes + }, + async teardown () { + // Clean up any resources created by setup() + await Promise.all(pubsubNodes.map(ps => ps.stop())) + peers.length && await Promise.all(peers.map(peer => peer.stop())) + } + }) +}) +``` diff --git a/packages/interfaces/src/pubsub/errors.js b/packages/libp2p-pubsub/src/errors.ts similarity index 97% rename from packages/interfaces/src/pubsub/errors.js rename to packages/libp2p-pubsub/src/errors.ts index 3c6235229..6d76ae57a 100644 --- a/packages/interfaces/src/pubsub/errors.js +++ b/packages/libp2p-pubsub/src/errors.ts @@ -1,6 +1,5 @@ -'use strict' -exports.codes = { +export const codes = { /** * Signature policy is invalid */ diff --git a/packages/interfaces/src/pubsub/index.js b/packages/libp2p-pubsub/src/index.ts similarity index 55% rename from packages/interfaces/src/pubsub/index.js rename to packages/libp2p-pubsub/src/index.ts index d2ca26453..56ad9925a 100644 --- a/packages/interfaces/src/pubsub/index.js +++ b/packages/libp2p-pubsub/src/index.ts @@ -1,176 +1,102 @@ -'use strict' - -const debug = require('debug') -const { EventEmitter } = require('events') -const errcode = require('err-code') - -const { pipe } = require('it-pipe') -const { default: Queue } = require('p-queue') - -const MulticodecTopology = require('../topology/multicodec-topology') -const { codes } = require('./errors') - -const { RPC } = require('./message/rpc') -const PeerStreams = require('./peer-streams') -const { SignaturePolicy } = require('./signature-policy') -const utils = require('./utils') - -const { +import debug from 'debug' +import { EventEmitter } from 'events' +import errcode from 'err-code' +import { pipe } from 'it-pipe' +import Queue from 'p-queue' +import { MulticodecTopology } from 'libp2p-topology/multicodec-topology' +import { codes } from './errors.js' +import { RPC, IRPC } from './message/rpc.js' +import { PeerStreams } from './peer-streams.js' +import * as utils from './utils.js' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { Registrar, IncomingStreamEvent } from 'libp2p-interfaces/registrar' +import type { Connection } from 'libp2p-interfaces/connection' +import type BufferList from 'bl' +import { signMessage, verifySignature -} = require('./message/sign') +} from './message/sign.js' +import type { PubSub, Message, StrictNoSign, StrictSign, PubsubOptions } from 'libp2p-interfaces/pubsub' -/** - * @typedef {any} Libp2p - * @typedef {import('peer-id')} PeerId - * @typedef {import('bl')} BufferList - * @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream - * @typedef {import('../connection/connection')} Connection - * @typedef {import('./signature-policy').SignaturePolicyType} SignaturePolicyType - * @typedef {import('./message/rpc').IRPC} IRPC - * @typedef {import('./message/rpc').RPC.SubOpts} RPCSubOpts - * @typedef {import('./message/rpc').RPC.Message} RPCMessage - */ - -/** - * @typedef {Object} InMessage - * @property {string} [from] - * @property {string} receivedFrom - * @property {string[]} topicIDs - * @property {Uint8Array} [seqno] - * @property {Uint8Array} data - * @property {Uint8Array} [signature] - * @property {Uint8Array} [key] - * - * @typedef {Object} PubsubProperties - * @property {string} debugName - log namespace - * @property {Array|string} multicodecs - protocol identificers to connect - * @property {Libp2p} libp2p - * - * @typedef {Object} PubsubOptions - * @property {SignaturePolicyType} [globalSignaturePolicy = SignaturePolicy.StrictSign] - defines how signatures should be handled - * @property {boolean} [canRelayMessage = false] - if can relay messages not subscribed - * @property {boolean} [emitSelf = false] - if publish should emit to self, if subscribed - * @property {number} [messageProcessingConcurrency = 10] - handle this many incoming pubsub messages concurrently - */ +export interface TopicValidator { (topic: string, message: Message): Promise } /** * PubsubBaseProtocol handles the peers and connections logic for pubsub routers * and specifies the API that pubsub routers should have. */ -class PubsubBaseProtocol extends EventEmitter { - /** - * @param {PubsubProperties & PubsubOptions} props - * @abstract - */ - constructor ({ - debugName, - multicodecs, - libp2p, - globalSignaturePolicy = SignaturePolicy.StrictSign, - canRelayMessage = false, - emitSelf = false, - messageProcessingConcurrency = 10 - }) { - if (typeof debugName !== 'string') { - throw new Error('a debugname `string` is required') - } - - if (!multicodecs) { - throw new Error('multicodecs are required') - } +export abstract class PubsubBaseProtocol extends EventEmitter implements PubSub { + public peerId: PeerId + public started: boolean + /** + * Map of topics to which peers are subscribed to + */ + public topics: Map> + /** + * List of our subscriptions + */ + public subscriptions: Set + /** + * Map of peer streams + */ + public peers: Map + /** + * The signature policy to follow by default + */ + public globalSignaturePolicy: StrictNoSign | StrictSign + /** + * If router can relay received messages, even if not subscribed + */ + public canRelayMessage: boolean + /** + * if publish should emit to self, if subscribed + */ + public emitSelf: boolean + /** + * Topic validator map + * + * Keyed by topic + * Topic validators are functions with the following input: + */ + public topicValidators: Map + public queue: Queue + public registrar: Registrar - if (!libp2p) { - throw new Error('libp2p is required') - } + protected log: debug.Debugger & { err: debug.Debugger } + protected multicodecs: string[] + protected _libp2p: any + private _registrarId: string | undefined + constructor (props: PubsubOptions) { super() + const { + debugName = 'libp2p:pubsub', + multicodecs = [], + libp2p = null, + globalSignaturePolicy = 'StrictSign', + canRelayMessage = false, + emitSelf = false, + messageProcessingConcurrency = 10 + } = props + this.log = Object.assign(debug(debugName), { err: debug(`${debugName}:error`) }) - /** - * @type {Array} - */ this.multicodecs = utils.ensureArray(multicodecs) this._libp2p = libp2p this.registrar = libp2p.registrar - /** - * @type {PeerId} - */ this.peerId = libp2p.peerId - this.started = false - - /** - * Map of topics to which peers are subscribed to - * - * @type {Map>} - */ this.topics = new Map() - - /** - * List of our subscriptions - * - * @type {Set} - */ this.subscriptions = new Set() - - /** - * Map of peer streams - * - * @type {Map} - */ this.peers = new Map() - - // validate signature policy - if (!SignaturePolicy[globalSignaturePolicy]) { - throw errcode(new Error('Invalid global signature policy'), codes.ERR_INVALID_SIGNATURE_POLICY) - } - - /** - * The signature policy to follow by default - * - * @type {string} - */ - this.globalSignaturePolicy = globalSignaturePolicy - - /** - * If router can relay received messages, even if not subscribed - * - * @type {boolean} - */ + this.globalSignaturePolicy = globalSignaturePolicy === 'StrictNoSign' ? 'StrictNoSign' : 'StrictSign' this.canRelayMessage = canRelayMessage - - /** - * if publish should emit to self, if subscribed - * - * @type {boolean} - */ this.emitSelf = emitSelf - - /** - * Topic validator function - * - * @typedef {function(string, InMessage): Promise} validator - */ - /** - * Topic validator map - * - * Keyed by topic - * Topic validators are functions with the following input: - * - * @type {Map} - */ this.topicValidators = new Map() - - /** - * @type {Queue} - */ this.queue = new Queue({ concurrency: messageProcessingConcurrency }) - this._registrarId = undefined this._onIncomingStream = this._onIncomingStream.bind(this) this._onPeerConnected = this._onPeerConnected.bind(this) this._onPeerDisconnected = this._onPeerDisconnected.bind(this) @@ -187,6 +113,7 @@ class PubsubBaseProtocol extends EventEmitter { if (this.started) { return } + this.log('starting') // Incoming streams @@ -219,7 +146,9 @@ class PubsubBaseProtocol extends EventEmitter { } // unregister protocol and handlers - this.registrar.unregister(this._registrarId) + if (this._registrarId != null) { + this.registrar.unregister(this._registrarId) + } this.log('stopping') this.peers.forEach((peerStreams) => peerStreams.close()) @@ -231,31 +160,22 @@ class PubsubBaseProtocol extends EventEmitter { } /** - * On an inbound stream opened. - * - * @protected - * @param {Object} props - * @param {string} props.protocol - * @param {MuxedStream} props.stream - * @param {Connection} props.connection - connection + * On an inbound stream opened */ - _onIncomingStream ({ protocol, stream, connection }) { + protected _onIncomingStream ({ protocol, stream, connection }: IncomingStreamEvent) { const peerId = connection.remotePeer const idB58Str = peerId.toB58String() const peer = this._addPeer(peerId, protocol) const inboundStream = peer.attachInboundStream(stream) this._processMessages(idB58Str, inboundStream, peer) + .catch(err => this.log(err)) } /** - * Registrar notifies an established connection with pubsub protocol. - * - * @protected - * @param {PeerId} peerId - remote peer-id - * @param {Connection} conn - connection to the peer + * Registrar notifies an established connection with pubsub protocol */ - async _onPeerConnected (peerId, conn) { + protected async _onPeerConnected (peerId: PeerId, conn: Connection) { const idB58Str = peerId.toB58String() this.log('connected', idB58Str) @@ -263,7 +183,7 @@ class PubsubBaseProtocol extends EventEmitter { const { stream, protocol } = await conn.newStream(this.multicodecs) const peer = this._addPeer(peerId, protocol) await peer.attachOutboundStream(stream) - } catch (err) { + } catch (err: any) { this.log.err(err) } @@ -272,33 +192,24 @@ class PubsubBaseProtocol extends EventEmitter { } /** - * Registrar notifies a closing connection with pubsub protocol. - * - * @protected - * @param {PeerId} peerId - peerId - * @param {Error} [err] - error for connection end + * Registrar notifies a closing connection with pubsub protocol */ - _onPeerDisconnected (peerId, err) { + protected _onPeerDisconnected (peerId: PeerId, conn?: Connection) { const idB58Str = peerId.toB58String() - this.log('connection ended', idB58Str, err ? err.message : '') + this.log('connection ended', idB58Str) this._removePeer(peerId) } /** * Notifies the router that a peer has been connected - * - * @protected - * @param {PeerId} peerId - * @param {string} protocol - * @returns {PeerStreams} */ - _addPeer (peerId, protocol) { + protected _addPeer (peerId: PeerId, protocol: string) { const id = peerId.toB58String() const existing = this.peers.get(id) // If peer streams already exists, do nothing - if (existing) { + if (existing != null) { return existing } @@ -317,17 +228,12 @@ class PubsubBaseProtocol extends EventEmitter { } /** - * Notifies the router that a peer has been disconnected. - * - * @protected - * @param {PeerId} peerId - * @returns {PeerStreams | undefined} + * Notifies the router that a peer has been disconnected */ - _removePeer (peerId) { - if (!peerId) return + protected _removePeer (peerId: PeerId) { const id = peerId.toB58String() const peerStreams = this.peers.get(id) - if (!peerStreams) return + if (peerStreams == null) return // close peer streams peerStreams.removeAllListeners() @@ -349,13 +255,8 @@ class PubsubBaseProtocol extends EventEmitter { /** * Responsible for processing each RPC message received by other peers. - * - * @param {string} idB58Str - peer id string in base58 - * @param {AsyncIterable} stream - inbound stream - * @param {PeerStreams} peerStreams - PubSub peer - * @returns {Promise} */ - async _processMessages (idB58Str, stream, peerStreams) { + async _processMessages (idB58Str: string, stream: AsyncIterable, peerStreams: PeerStreams) { try { await pipe( stream, @@ -368,35 +269,25 @@ class PubsubBaseProtocol extends EventEmitter { // the simplest/safest option here is to wrap in a function and capture all errors // to prevent a top-level unhandled exception // This processing of rpc messages should happen without awaiting full validation/execution of prior messages - ;(async () => { - try { - await this._processRpc(idB58Str, peerStreams, rpcMsg) - } catch (err) { - this.log.err(err) - } - })() + this._processRpc(idB58Str, peerStreams, rpcMsg) + .catch(err => this.log(err)) } } ) - } catch (err) { + } catch (err: any) { this._onPeerDisconnected(peerStreams.id, err) } } /** * Handles an rpc request from a peer - * - * @param {string} idB58Str - * @param {PeerStreams} peerStreams - * @param {RPC} rpc - * @returns {Promise} */ - async _processRpc (idB58Str, peerStreams, rpc) { + async _processRpc (idB58Str: string, peerStreams: PeerStreams, rpc: RPC) { this.log('rpc from', idB58Str) const subs = rpc.subscriptions const msgs = rpc.msgs - if (subs.length) { + if (subs.length > 0) { // update peer subscriptions subs.forEach((subOpt) => { this._processRpcSubOpt(idB58Str, subOpt) @@ -409,9 +300,12 @@ class PubsubBaseProtocol extends EventEmitter { return false } - if (msgs.length) { + if (msgs.length > 0) { this.queue.addAll(msgs.map(message => async () => { - if (!(this.canRelayMessage || (message.topicIDs && message.topicIDs.some((topic) => this.subscriptions.has(topic))))) { + const topics = message.topicIDs != null ? message.topicIDs : [] + const hasSubscription = topics.some((topic) => this.subscriptions.has(topic)) + + if (!hasSubscription && !this.canRelayMessage) { this.log('received message we didn\'t subscribe to. Dropping.') return } @@ -420,34 +314,32 @@ class PubsubBaseProtocol extends EventEmitter { const msg = utils.normalizeInRpcMessage(message, idB58Str) await this._processRpcMessage(msg) - } catch (err) { + } catch (err: any) { this.log.err(err) } })) + .catch(err => this.log(err)) } return true } /** * Handles a subscription change from a peer - * - * @param {string} id - * @param {RPC.ISubOpts} subOpt */ - _processRpcSubOpt (id, subOpt) { + _processRpcSubOpt (id: string, subOpt: RPC.ISubOpts) { const t = subOpt.topicID - if (!t) { + if (t == null) { return } let topicSet = this.topics.get(t) - if (!topicSet) { + if (topicSet == null) { topicSet = new Set() this.topics.set(t, topicSet) } - if (subOpt.subscribe) { + if (subOpt.subscribe === true) { // subscribe peer to new topic topicSet.add(id) } else { @@ -458,19 +350,16 @@ class PubsubBaseProtocol extends EventEmitter { /** * Handles an message from a peer - * - * @param {InMessage} msg - * @returns {Promise} */ - async _processRpcMessage (msg) { - if (this.peerId.toB58String() === msg.from && !this.emitSelf) { + async _processRpcMessage (msg: Message) { + if ((msg.from != null) && this.peerId.equals(msg.from) && !this.emitSelf) { return } // Ensure the message is valid before processing it try { await this.validate(msg) - } catch (err) { + } catch (err: any) { this.log('Message is invalid, dropping it. %O', err) return } @@ -478,15 +367,13 @@ class PubsubBaseProtocol extends EventEmitter { // Emit to self this._emitMessage(msg) - return this._publish(utils.normalizeOutRpcMessage(msg)) + return await this._publish(utils.normalizeOutRpcMessage(msg)) } /** * Emit a message from a peer - * - * @param {InMessage} message */ - _emitMessage (message) { + _emitMessage (message: Message) { message.topicIDs.forEach((topic) => { if (this.subscriptions.has(topic)) { this.emit(topic, message) @@ -497,66 +384,50 @@ class PubsubBaseProtocol extends EventEmitter { /** * The default msgID implementation * Child class can override this. - * - * @param {InMessage} msg - the message object - * @returns {Promise | Uint8Array} message id as bytes */ - getMsgId (msg) { + getMsgId (msg: Message) { const signaturePolicy = this.globalSignaturePolicy switch (signaturePolicy) { - case SignaturePolicy.StrictSign: - // @ts-ignore seqno is optional in protobuf definition but it will exist + case 'StrictSign': + // @ts-expect-error seqno is optional in protobuf definition but it will exist return utils.msgId(msg.from, msg.seqno) - case SignaturePolicy.StrictNoSign: + case 'StrictNoSign': return utils.noSignMsgId(msg.data) default: - throw errcode(new Error('Cannot get message id: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY) + throw errcode(new Error('Cannot get message id: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY) } } /** * Whether to accept a message from a peer * Override to create a graylist - * - * @param {string} id - * @returns {boolean} */ - _acceptFrom (id) { + _acceptFrom (id: string) { return true } /** * Decode Uint8Array into an RPC object. * This can be override to use a custom router protobuf. - * - * @param {Uint8Array} bytes - * @returns {RPC} */ - _decodeRpc (bytes) { + _decodeRpc (bytes: Uint8Array) { return RPC.decode(bytes) } /** * Encode RPC object into a Uint8Array. * This can be override to use a custom router protobuf. - * - * @param {IRPC} rpc - * @returns {Uint8Array} */ - _encodeRpc (rpc) { + _encodeRpc (rpc: IRPC) { return RPC.encode(rpc).finish() } /** * Send an rpc object to a peer - * - * @param {string} id - peer id - * @param {IRPC} rpc - * @returns {void} */ - _sendRpc (id, rpc) { + _sendRpc (id: string, rpc: IRPC) { const peerStreams = this.peers.get(id) - if (!peerStreams || !peerStreams.isWritable) { + if ((peerStreams == null) || !peerStreams.isWritable) { const msg = `Cannot send RPC to ${id} as there is no open stream to it available` this.log.err(msg) @@ -566,14 +437,9 @@ class PubsubBaseProtocol extends EventEmitter { } /** - * Send subscroptions to a peer - * - * @param {string} id - peer id - * @param {string[]} topics - * @param {boolean} subscribe - set to false for unsubscriptions - * @returns {void} + * Send subscriptions to a peer */ - _sendSubscriptions (id, topics, subscribe) { + _sendSubscriptions (id: string, topics: string[], subscribe: boolean) { return this._sendRpc(id, { subscriptions: topics.map(t => ({ topicID: t, subscribe: subscribe })) }) @@ -582,32 +448,29 @@ class PubsubBaseProtocol extends EventEmitter { /** * Validates the given message. The signature will be checked for authenticity. * Throws an error on invalid messages - * - * @param {InMessage} message - * @returns {Promise} */ - async validate (message) { // eslint-disable-line require-await + async validate (message: Message) { // eslint-disable-line require-await const signaturePolicy = this.globalSignaturePolicy switch (signaturePolicy) { - case SignaturePolicy.StrictNoSign: - if (message.from) { + case 'StrictNoSign': + if (message.from != null) { throw errcode(new Error('StrictNoSigning: from should not be present'), codes.ERR_UNEXPECTED_FROM) } - if (message.signature) { + if (message.signature != null) { throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE) } - if (message.key) { + if (message.key != null) { throw errcode(new Error('StrictNoSigning: key should not be present'), codes.ERR_UNEXPECTED_KEY) } - if (message.seqno) { + if (message.seqno != null) { throw errcode(new Error('StrictNoSigning: seqno should not be present'), codes.ERR_UNEXPECTED_SEQNO) } break - case SignaturePolicy.StrictSign: - if (!message.signature) { + case 'StrictSign': + if (message.signature == null) { throw errcode(new Error('StrictSigning: Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE) } - if (!message.seqno) { + if (message.seqno == null) { throw errcode(new Error('StrictSigning: Signing required and no seqno was present'), codes.ERR_MISSING_SEQNO) } if (!(await verifySignature(message))) { @@ -615,12 +478,12 @@ class PubsubBaseProtocol extends EventEmitter { } break default: - throw errcode(new Error('Cannot validate message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY) + throw errcode(new Error('Cannot validate message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY) } for (const topic of message.topicIDs) { const validatorFn = this.topicValidators.get(topic) - if (validatorFn) { + if (validatorFn != null) { await validatorFn(topic, message) } } @@ -629,22 +492,18 @@ class PubsubBaseProtocol extends EventEmitter { /** * Normalizes the message and signs it, if signing is enabled. * Should be used by the routers to create the message to send. - * - * @protected - * @param {InMessage} message - * @returns {Promise} */ - _buildMessage (message) { + protected async _buildMessage (message: Message) { const signaturePolicy = this.globalSignaturePolicy switch (signaturePolicy) { - case SignaturePolicy.StrictSign: - message.from = this.peerId.toB58String() + case 'StrictSign': + message.from = this.peerId.toBytes() message.seqno = utils.randomSeqno() - return signMessage(this.peerId, message) - case SignaturePolicy.StrictNoSign: - return Promise.resolve(message) + return await signMessage(this.peerId, message) + case 'StrictNoSign': + return await Promise.resolve(message) default: - throw errcode(new Error('Cannot build message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY) + throw errcode(new Error('Cannot build message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY) } } @@ -652,34 +511,29 @@ class PubsubBaseProtocol extends EventEmitter { /** * Get a list of the peer-ids that are subscribed to one topic. - * - * @param {string} topic - * @returns {Array} */ - getSubscribers (topic) { + getSubscribers (topic: string) { if (!this.started) { throw errcode(new Error('not started yet'), 'ERR_NOT_STARTED_YET') } - if (!topic || typeof topic !== 'string') { - throw errcode(new Error('a string topic must be provided'), 'ERR_NOT_VALID_TOPIC') + if (topic == null) { + throw errcode(new Error('topic is required'), 'ERR_NOT_VALID_TOPIC') } const peersInTopic = this.topics.get(topic) - if (!peersInTopic) { + + if (peersInTopic == null) { return [] } + return Array.from(peersInTopic) } /** * Publishes messages to all subscribed peers - * - * @param {string} topic - * @param {Uint8Array} message - * @returns {Promise} */ - async publish (topic, message) { + async publish (topic: string, message: Uint8Array) { if (!this.started) { throw new Error('Pubsub has not started') } @@ -695,7 +549,6 @@ class PubsubBaseProtocol extends EventEmitter { // ensure that the message follows the signature policy const outMsg = await this._buildMessage(msgObject) - // @ts-ignore different type as from is converted const msg = utils.normalizeInRpcMessage(outMsg) // Emit to self if I'm interested and emitSelf enabled @@ -708,24 +561,13 @@ class PubsubBaseProtocol extends EventEmitter { /** * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation. * For example, a Floodsub implementation might simply publish each message to each topic for every peer - * - * @abstract - * @param {InMessage|RPCMessage} message - * @returns {Promise} - * */ - _publish (message) { - throw errcode(new Error('publish must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') - } + abstract _publish (message: Message): Promise /** * Subscribes to a given topic. - * - * @abstract - * @param {string} topic - * @returns {void} */ - subscribe (topic) { + subscribe (topic: string) { if (!this.started) { throw new Error('Pubsub has not started') } @@ -738,11 +580,8 @@ class PubsubBaseProtocol extends EventEmitter { /** * Unsubscribe from the given topic. - * - * @param {string} topic - * @returns {void} */ - unsubscribe (topic) { + unsubscribe (topic: string) { if (!this.started) { throw new Error('Pubsub is not started') } @@ -755,8 +594,6 @@ class PubsubBaseProtocol extends EventEmitter { /** * Get the list of topics which the peer is subscribed to. - * - * @returns {Array} */ getTopics () { if (!this.started) { @@ -766,8 +603,3 @@ class PubsubBaseProtocol extends EventEmitter { return Array.from(this.subscriptions) } } - -PubsubBaseProtocol.utils = utils -PubsubBaseProtocol.SignaturePolicy = SignaturePolicy - -module.exports = PubsubBaseProtocol diff --git a/packages/libp2p-pubsub/src/message/rpc.d.ts b/packages/libp2p-pubsub/src/message/rpc.d.ts new file mode 100644 index 000000000..420e69542 --- /dev/null +++ b/packages/libp2p-pubsub/src/message/rpc.d.ts @@ -0,0 +1,258 @@ +import * as $protobuf from 'protobufjs' +/** Properties of a RPC. */ +export interface IRPC { + + /** RPC subscriptions */ + subscriptions?: (RPC.ISubOpts[]|null) + + /** RPC msgs */ + msgs?: (RPC.IMessage[]|null) +} + +/** Represents a RPC. */ +export class RPC implements IRPC { + /** + * Constructs a new RPC. + * + * @param [p] - Properties to set + */ + constructor (p?: IRPC); + + /** RPC subscriptions. */ + public subscriptions: RPC.ISubOpts[] + + /** RPC msgs. */ + public msgs: RPC.IMessage[] + + /** + * Encodes the specified RPC message. Does not implicitly {@link RPC.verify|verify} messages. + * + * @param m - RPC message or plain object to encode + * @param [w] - Writer to encode to + * @returns Writer + */ + public static encode (m: IRPC, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a RPC message from the specified reader or buffer. + * + * @param r - Reader or buffer to decode from + * @param [l] - Message length if known beforehand + * @returns RPC + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode (r: ($protobuf.Reader|Uint8Array), l?: number): RPC; + + /** + * Creates a RPC message from a plain object. Also converts values to their respective internal types. + * + * @param d - Plain object + * @returns RPC + */ + public static fromObject (d: { [k: string]: any }): RPC; + + /** + * Creates a plain object from a RPC message. Also converts values to other types if specified. + * + * @param m - RPC + * @param [o] - Conversion options + * @returns Plain object + */ + public static toObject (m: RPC, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this RPC to JSON. + * + * @returns JSON object + */ + public toJSON (): { [k: string]: any }; +} + +export namespace RPC { + + /** Properties of a SubOpts. */ + interface ISubOpts { + + /** SubOpts subscribe */ + subscribe?: (boolean|null) + + /** SubOpts topicID */ + topicID?: (string|null) + } + + /** Represents a SubOpts. */ + class SubOpts implements ISubOpts { + /** + * Constructs a new SubOpts. + * + * @param [p] - Properties to set + */ + constructor (p?: RPC.ISubOpts); + + /** SubOpts subscribe. */ + public subscribe?: (boolean|null) + + /** SubOpts topicID. */ + public topicID?: (string|null) + + /** SubOpts _subscribe. */ + public _subscribe?: 'subscribe' + + /** SubOpts _topicID. */ + public _topicID?: 'topicID' + + /** + * Encodes the specified SubOpts message. Does not implicitly {@link RPC.SubOpts.verify|verify} messages. + * + * @param m - SubOpts message or plain object to encode + * @param [w] - Writer to encode to + * @returns Writer + */ + public static encode (m: RPC.ISubOpts, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a SubOpts message from the specified reader or buffer. + * + * @param r - Reader or buffer to decode from + * @param [l] - Message length if known beforehand + * @returns SubOpts + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode (r: ($protobuf.Reader|Uint8Array), l?: number): RPC.SubOpts; + + /** + * Creates a SubOpts message from a plain object. Also converts values to their respective internal types. + * + * @param d - Plain object + * @returns SubOpts + */ + public static fromObject (d: { [k: string]: any }): RPC.SubOpts; + + /** + * Creates a plain object from a SubOpts message. Also converts values to other types if specified. + * + * @param m - SubOpts + * @param [o] - Conversion options + * @returns Plain object + */ + public static toObject (m: RPC.SubOpts, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this SubOpts to JSON. + * + * @returns JSON object + */ + public toJSON (): { [k: string]: any }; + } + + /** Properties of a Message. */ + interface IMessage { + + /** Message from */ + from?: (Uint8Array|null) + + /** Message data */ + data?: (Uint8Array|null) + + /** Message seqno */ + seqno?: (Uint8Array|null) + + /** Message topicIDs */ + topicIDs?: (string[]|null) + + /** Message signature */ + signature?: (Uint8Array|null) + + /** Message key */ + key?: (Uint8Array|null) + } + + /** Represents a Message. */ + class Message implements IMessage { + /** + * Constructs a new Message. + * + * @param [p] - Properties to set + */ + constructor (p?: RPC.IMessage); + + /** Message from. */ + public from?: (Uint8Array|null) + + /** Message data. */ + public data?: (Uint8Array|null) + + /** Message seqno. */ + public seqno?: (Uint8Array|null) + + /** Message topicIDs. */ + public topicIDs: string[] + + /** Message signature. */ + public signature?: (Uint8Array|null) + + /** Message key. */ + public key?: (Uint8Array|null) + + /** Message _from. */ + public _from?: 'from' + + /** Message _data. */ + public _data?: 'data' + + /** Message _seqno. */ + public _seqno?: 'seqno' + + /** Message _signature. */ + public _signature?: 'signature' + + /** Message _key. */ + public _key?: 'key' + + /** + * Encodes the specified Message message. Does not implicitly {@link RPC.Message.verify|verify} messages. + * + * @param m - Message message or plain object to encode + * @param [w] - Writer to encode to + * @returns Writer + */ + public static encode (m: RPC.IMessage, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a Message message from the specified reader or buffer. + * + * @param r - Reader or buffer to decode from + * @param [l] - Message length if known beforehand + * @returns Message + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode (r: ($protobuf.Reader|Uint8Array), l?: number): RPC.Message; + + /** + * Creates a Message message from a plain object. Also converts values to their respective internal types. + * + * @param d - Plain object + * @returns Message + */ + public static fromObject (d: { [k: string]: any }): RPC.Message; + + /** + * Creates a plain object from a Message message. Also converts values to other types if specified. + * + * @param m - Message + * @param [o] - Conversion options + * @returns Plain object + */ + public static toObject (m: RPC.Message, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this Message to JSON. + * + * @returns JSON object + */ + public toJSON (): { [k: string]: any }; + } +} diff --git a/packages/interfaces/src/pubsub/message/rpc.js b/packages/libp2p-pubsub/src/message/rpc.js similarity index 98% rename from packages/interfaces/src/pubsub/message/rpc.js rename to packages/libp2p-pubsub/src/message/rpc.js index e46d601aa..edf013e3d 100644 --- a/packages/interfaces/src/pubsub/message/rpc.js +++ b/packages/libp2p-pubsub/src/message/rpc.js @@ -1,15 +1,13 @@ /*eslint-disable*/ -"use strict"; - -var $protobuf = require("protobufjs/minimal"); +import $protobuf from "protobufjs/minimal.js"; // Common aliases -var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; +const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; // Exported root namespace -var $root = $protobuf.roots["libp2p-pubsub-rpc"] || ($protobuf.roots["libp2p-pubsub-rpc"] = {}); +const $root = $protobuf.roots["libp2p-pubsub-rpc"] || ($protobuf.roots["libp2p-pubsub-rpc"] = {}); -$root.RPC = (function() { +export const RPC = $root.RPC = (() => { /** * Properties of a RPC. @@ -231,7 +229,7 @@ $root.RPC = (function() { SubOpts.prototype.topicID = null; // OneOf field names bound to virtual getters and setters - var $oneOfFields; + let $oneOfFields; /** * SubOpts _subscribe. @@ -446,7 +444,7 @@ $root.RPC = (function() { Message.prototype.key = null; // OneOf field names bound to virtual getters and setters - var $oneOfFields; + let $oneOfFields; /** * Message _from. @@ -698,4 +696,4 @@ $root.RPC = (function() { return RPC; })(); -module.exports = $root; +export { $root as default }; diff --git a/packages/interfaces/src/pubsub/message/rpc.proto b/packages/libp2p-pubsub/src/message/rpc.proto similarity index 100% rename from packages/interfaces/src/pubsub/message/rpc.proto rename to packages/libp2p-pubsub/src/message/rpc.proto diff --git a/packages/interfaces/src/pubsub/message/sign.js b/packages/libp2p-pubsub/src/message/sign.ts similarity index 54% rename from packages/interfaces/src/pubsub/message/sign.js rename to packages/libp2p-pubsub/src/message/sign.ts index 21b89c2a0..a4e280a82 100644 --- a/packages/interfaces/src/pubsub/message/sign.js +++ b/packages/libp2p-pubsub/src/message/sign.ts @@ -1,51 +1,51 @@ -'use strict' +import PeerIdFactory from 'peer-id' +import { RPC } from './rpc.js' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { normalizeOutRpcMessage } from '../utils.js' +import type { Message } from 'libp2p-interfaces/pubsub' +import type { PeerId } from 'libp2p-interfaces/peer-id' -const PeerId = require('peer-id') -const { RPC } = require('./rpc') -const { concat: uint8ArrayConcat } = require('uint8arrays/concat') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const SignPrefix = uint8ArrayFromString('libp2p-pubsub:') -const { normalizeOutRpcMessage } = require('../utils') - -/** - * @typedef {import('..').InMessage} - */ +export const SignPrefix = uint8ArrayFromString('libp2p-pubsub:') /** * Signs the provided message with the given `peerId` - * - * @param {PeerId} peerId - * @param {InMessage} message - * @returns {Promise} */ -async function signMessage (peerId, message) { +export async function signMessage (peerId: PeerId, message: Message) { // Get the message in bytes, and prepend with the pubsub prefix const bytes = uint8ArrayConcat([ SignPrefix, RPC.Message.encode(normalizeOutRpcMessage(message)).finish() ]) + if (peerId.privKey == null) { + throw new Error('Cannot sign message, no private key present') + } + + if (peerId.pubKey == null) { + throw new Error('Cannot sign message, no public key present') + } + const signature = await peerId.privKey.sign(bytes) - return { + const outputMessage: Message = { ...message, signature: signature, key: peerId.pubKey.bytes } + + return outputMessage } /** * Verifies the signature of the given message - * - * @param {InMessage} message - * @returns {Promise} */ -async function verifySignature (message) { - if (!message.signature) { +export async function verifySignature (message: Message) { + if (message.signature == null) { throw new Error('Message must contain a signature to be verified') } - if (!message.from) { + if (message.from == null) { throw new Error('Message must contain a from property to be verified') } @@ -54,7 +54,7 @@ async function verifySignature (message) { SignPrefix, RPC.Message.encode({ ...message, - from: PeerId.createFromB58String(message.from).toBytes(), + from: PeerIdFactory.createFromBytes(message.from).toBytes(), signature: undefined, key: undefined }).finish() @@ -64,46 +64,31 @@ async function verifySignature (message) { const pubKey = await messagePublicKey(message) // verify the base message - return pubKey.verify(bytes, message.signature) + return await pubKey.verify(bytes, message.signature) } /** * Returns the PublicKey associated with the given message. * If no, valid PublicKey can be retrieved an error will be returned. - * - * @param {InMessage} message - * @returns {Promise} */ -async function messagePublicKey (message) { +export async function messagePublicKey (message: Message) { // should be available in the from property of the message (peer id) - if (!message.from) { + if (message.from == null) { throw new Error('Could not get the public key from the originator id') } - const from = PeerId.createFromB58String(message.from) + const from = PeerIdFactory.createFromBytes(message.from) - if (message.key) { - const keyPeerId = await PeerId.createFromPubKey(message.key) + if (message.key != null) { + const keyPeerId = await PeerIdFactory.createFromPubKey(message.key) // the key belongs to the sender, return the key if (keyPeerId.equals(from)) return keyPeerId.pubKey // We couldn't validate pubkey is from the originator, error throw new Error('Public Key does not match the originator') - } else if (from.pubKey) { + } else if (from.pubKey != null) { return from.pubKey } else { throw new Error('Could not get the public key from the originator id') } } - -/** - * @typedef {import('..').InMessage} InMessage - * @typedef {import('libp2p-crypto').PublicKey} PublicKey - */ - -module.exports = { - messagePublicKey, - signMessage, - SignPrefix, - verifySignature -} diff --git a/packages/libp2p-pubsub/src/message/topic-descriptor.d.ts b/packages/libp2p-pubsub/src/message/topic-descriptor.d.ts new file mode 100644 index 000000000..528396d4b --- /dev/null +++ b/packages/libp2p-pubsub/src/message/topic-descriptor.d.ts @@ -0,0 +1,254 @@ +import * as $protobuf from 'protobufjs' +/** Properties of a TopicDescriptor. */ +export interface ITopicDescriptor { + + /** TopicDescriptor name */ + name?: (string|null) + + /** TopicDescriptor auth */ + auth?: (TopicDescriptor.IAuthOpts|null) + + /** TopicDescriptor enc */ + enc?: (TopicDescriptor.IEncOpts|null) +} + +/** Represents a TopicDescriptor. */ +export class TopicDescriptor implements ITopicDescriptor { + /** + * Constructs a new TopicDescriptor. + * + * @param [p] - Properties to set + */ + constructor (p?: ITopicDescriptor); + + /** TopicDescriptor name. */ + public name?: (string|null) + + /** TopicDescriptor auth. */ + public auth?: (TopicDescriptor.IAuthOpts|null) + + /** TopicDescriptor enc. */ + public enc?: (TopicDescriptor.IEncOpts|null) + + /** TopicDescriptor _name. */ + public _name?: 'name' + + /** TopicDescriptor _auth. */ + public _auth?: 'auth' + + /** TopicDescriptor _enc. */ + public _enc?: 'enc' + + /** + * Encodes the specified TopicDescriptor message. Does not implicitly {@link TopicDescriptor.verify|verify} messages. + * + * @param m - TopicDescriptor message or plain object to encode + * @param [w] - Writer to encode to + * @returns Writer + */ + public static encode (m: ITopicDescriptor, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a TopicDescriptor message from the specified reader or buffer. + * + * @param r - Reader or buffer to decode from + * @param [l] - Message length if known beforehand + * @returns TopicDescriptor + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode (r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor; + + /** + * Creates a TopicDescriptor message from a plain object. Also converts values to their respective internal types. + * + * @param d - Plain object + * @returns TopicDescriptor + */ + public static fromObject (d: { [k: string]: any }): TopicDescriptor; + + /** + * Creates a plain object from a TopicDescriptor message. Also converts values to other types if specified. + * + * @param m - TopicDescriptor + * @param [o] - Conversion options + * @returns Plain object + */ + public static toObject (m: TopicDescriptor, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this TopicDescriptor to JSON. + * + * @returns JSON object + */ + public toJSON (): { [k: string]: any }; +} + +export namespace TopicDescriptor { + + /** Properties of an AuthOpts. */ + interface IAuthOpts { + + /** AuthOpts mode */ + mode?: (TopicDescriptor.AuthOpts.AuthMode|null) + + /** AuthOpts keys */ + keys?: (Uint8Array[]|null) + } + + /** Represents an AuthOpts. */ + class AuthOpts implements IAuthOpts { + /** + * Constructs a new AuthOpts. + * + * @param [p] - Properties to set + */ + constructor (p?: TopicDescriptor.IAuthOpts); + + /** AuthOpts mode. */ + public mode?: (TopicDescriptor.AuthOpts.AuthMode|null) + + /** AuthOpts keys. */ + public keys: Uint8Array[] + + /** AuthOpts _mode. */ + public _mode?: 'mode' + + /** + * Encodes the specified AuthOpts message. Does not implicitly {@link TopicDescriptor.AuthOpts.verify|verify} messages. + * + * @param m - AuthOpts message or plain object to encode + * @param [w] - Writer to encode to + * @returns Writer + */ + public static encode (m: TopicDescriptor.IAuthOpts, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes an AuthOpts message from the specified reader or buffer. + * + * @param r - Reader or buffer to decode from + * @param [l] - Message length if known beforehand + * @returns AuthOpts + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode (r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor.AuthOpts; + + /** + * Creates an AuthOpts message from a plain object. Also converts values to their respective internal types. + * + * @param d - Plain object + * @returns AuthOpts + */ + public static fromObject (d: { [k: string]: any }): TopicDescriptor.AuthOpts; + + /** + * Creates a plain object from an AuthOpts message. Also converts values to other types if specified. + * + * @param m - AuthOpts + * @param [o] - Conversion options + * @returns Plain object + */ + public static toObject (m: TopicDescriptor.AuthOpts, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this AuthOpts to JSON. + * + * @returns JSON object + */ + public toJSON (): { [k: string]: any }; + } + + namespace AuthOpts { + + /** AuthMode enum. */ + enum AuthMode { + NONE = 0, + KEY = 1, + WOT = 2 + } + } + + /** Properties of an EncOpts. */ + interface IEncOpts { + + /** EncOpts mode */ + mode?: (TopicDescriptor.EncOpts.EncMode|null) + + /** EncOpts keyHashes */ + keyHashes?: (Uint8Array[]|null) + } + + /** Represents an EncOpts. */ + class EncOpts implements IEncOpts { + /** + * Constructs a new EncOpts. + * + * @param [p] - Properties to set + */ + constructor (p?: TopicDescriptor.IEncOpts); + + /** EncOpts mode. */ + public mode?: (TopicDescriptor.EncOpts.EncMode|null) + + /** EncOpts keyHashes. */ + public keyHashes: Uint8Array[] + + /** EncOpts _mode. */ + public _mode?: 'mode' + + /** + * Encodes the specified EncOpts message. Does not implicitly {@link TopicDescriptor.EncOpts.verify|verify} messages. + * + * @param m - EncOpts message or plain object to encode + * @param [w] - Writer to encode to + * @returns Writer + */ + public static encode (m: TopicDescriptor.IEncOpts, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes an EncOpts message from the specified reader or buffer. + * + * @param r - Reader or buffer to decode from + * @param [l] - Message length if known beforehand + * @returns EncOpts + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode (r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor.EncOpts; + + /** + * Creates an EncOpts message from a plain object. Also converts values to their respective internal types. + * + * @param d - Plain object + * @returns EncOpts + */ + public static fromObject (d: { [k: string]: any }): TopicDescriptor.EncOpts; + + /** + * Creates a plain object from an EncOpts message. Also converts values to other types if specified. + * + * @param m - EncOpts + * @param [o] - Conversion options + * @returns Plain object + */ + public static toObject (m: TopicDescriptor.EncOpts, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this EncOpts to JSON. + * + * @returns JSON object + */ + public toJSON (): { [k: string]: any }; + } + + namespace EncOpts { + + /** EncMode enum. */ + enum EncMode { + NONE = 0, + SHAREDKEY = 1, + WOT = 2 + } + } +} diff --git a/packages/interfaces/src/pubsub/message/topic-descriptor.js b/packages/libp2p-pubsub/src/message/topic-descriptor.js similarity index 97% rename from packages/interfaces/src/pubsub/message/topic-descriptor.js rename to packages/libp2p-pubsub/src/message/topic-descriptor.js index 9758d570a..72b7e6520 100644 --- a/packages/interfaces/src/pubsub/message/topic-descriptor.js +++ b/packages/libp2p-pubsub/src/message/topic-descriptor.js @@ -1,15 +1,13 @@ /*eslint-disable*/ -"use strict"; - -var $protobuf = require("protobufjs/minimal"); +import $protobuf from "protobufjs/minimal.js"; // Common aliases -var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; +const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; // Exported root namespace -var $root = $protobuf.roots["libp2p-pubsub-topic-descriptor"] || ($protobuf.roots["libp2p-pubsub-topic-descriptor"] = {}); +const $root = $protobuf.roots["libp2p-pubsub-topic-descriptor"] || ($protobuf.roots["libp2p-pubsub-topic-descriptor"] = {}); -$root.TopicDescriptor = (function() { +export const TopicDescriptor = $root.TopicDescriptor = (() => { /** * Properties of a TopicDescriptor. @@ -60,7 +58,7 @@ $root.TopicDescriptor = (function() { TopicDescriptor.prototype.enc = null; // OneOf field names bound to virtual getters and setters - var $oneOfFields; + let $oneOfFields; /** * TopicDescriptor _name. @@ -264,7 +262,7 @@ $root.TopicDescriptor = (function() { AuthOpts.prototype.keys = $util.emptyArray; // OneOf field names bound to virtual getters and setters - var $oneOfFields; + let $oneOfFields; /** * AuthOpts _mode. @@ -422,7 +420,7 @@ $root.TopicDescriptor = (function() { * @property {number} WOT=2 WOT value */ AuthOpts.AuthMode = (function() { - var valuesById = {}, values = Object.create(valuesById); + const valuesById = {}, values = Object.create(valuesById); values[valuesById[0] = "NONE"] = 0; values[valuesById[1] = "KEY"] = 1; values[valuesById[2] = "WOT"] = 2; @@ -475,7 +473,7 @@ $root.TopicDescriptor = (function() { EncOpts.prototype.keyHashes = $util.emptyArray; // OneOf field names bound to virtual getters and setters - var $oneOfFields; + let $oneOfFields; /** * EncOpts _mode. @@ -633,7 +631,7 @@ $root.TopicDescriptor = (function() { * @property {number} WOT=2 WOT value */ EncOpts.EncMode = (function() { - var valuesById = {}, values = Object.create(valuesById); + const valuesById = {}, values = Object.create(valuesById); values[valuesById[0] = "NONE"] = 0; values[valuesById[1] = "SHAREDKEY"] = 1; values[valuesById[2] = "WOT"] = 2; @@ -646,4 +644,4 @@ $root.TopicDescriptor = (function() { return TopicDescriptor; })(); -module.exports = $root; +export { $root as default }; diff --git a/packages/interfaces/src/pubsub/message/topic-descriptor.proto b/packages/libp2p-pubsub/src/message/topic-descriptor.proto similarity index 100% rename from packages/interfaces/src/pubsub/message/topic-descriptor.proto rename to packages/libp2p-pubsub/src/message/topic-descriptor.proto diff --git a/packages/libp2p-pubsub/src/peer-streams.ts b/packages/libp2p-pubsub/src/peer-streams.ts new file mode 100644 index 000000000..8e3b479ef --- /dev/null +++ b/packages/libp2p-pubsub/src/peer-streams.ts @@ -0,0 +1,169 @@ +import debug from 'debug' +import { EventEmitter } from 'events' +import lp from 'it-length-prefixed' +import pushable from 'it-pushable' +import { pipe } from 'it-pipe' +import { source as abortable } from 'abortable-iterator' +import AbortController from 'abort-controller' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { MuxedStream } from 'libp2p-interfaces/stream-muxer' + +const log = Object.assign(debug('libp2p-pubsub:peer-streams'), { + error: debug('libp2p-pubsub:peer-streams:err') +}) + +export interface Options { + id: PeerId + protocol: string +} + +/** + * Thin wrapper around a peer's inbound / outbound pubsub streams + */ +export class PeerStreams extends EventEmitter { + public readonly id: PeerId + public readonly protocol: string + /** + * Write stream - it's preferable to use the write method + */ + public outboundStream: pushable.Pushable | undefined + /** + * Read stream + */ + public inboundStream: AsyncIterable | undefined + /** + * The raw outbound stream, as retrieved from conn.newStream + */ + private _rawOutboundStream: MuxedStream | undefined + /** + * The raw inbound stream, as retrieved from the callback from libp2p.handle + */ + private _rawInboundStream: MuxedStream | undefined + /** + * An AbortController for controlled shutdown of the inbound stream + */ + private readonly _inboundAbortController: AbortController + + constructor (opts: Options) { + super() + + this.id = opts.id + this.protocol = opts.protocol + + this._inboundAbortController = new AbortController() + } + + /** + * Do we have a connection to read from? + * + * @type {boolean} + */ + get isReadable () { + return Boolean(this.inboundStream) + } + + /** + * Do we have a connection to write on? + * + * @type {boolean} + */ + get isWritable () { + return Boolean(this.outboundStream) + } + + /** + * Send a message to this peer. + * Throws if there is no `stream` to write to available. + */ + write (data: Uint8Array) { + if (this.outboundStream == null) { + const id = this.id.toB58String() + throw new Error('No writable connection to ' + id) + } + + this.outboundStream.push(data) + } + + /** + * Attach a raw inbound stream and setup a read stream + */ + attachInboundStream (stream: MuxedStream) { + // Create and attach a new inbound stream + // The inbound stream is: + // - abortable, set to only return on abort, rather than throw + // - transformed with length-prefix transform + this._rawInboundStream = stream + this.inboundStream = abortable( + pipe( + this._rawInboundStream, + lp.decode() + ), + this._inboundAbortController.signal, + { returnOnAbort: true } + ) + + this.emit('stream:inbound') + return this.inboundStream + } + + /** + * Attach a raw outbound stream and setup a write stream + */ + async attachOutboundStream (stream: MuxedStream) { + // If an outbound stream already exists, gently close it + const _prevStream = this.outboundStream + if (this.outboundStream != null) { + // End the stream without emitting a close event + await this.outboundStream.end() + } + + this._rawOutboundStream = stream + this.outboundStream = pushable({ + onEnd: (shouldEmit) => { + // close writable side of the stream + if ((this._rawOutboundStream?.reset) != null) { + this._rawOutboundStream.reset() + } + + this._rawOutboundStream = undefined + this.outboundStream = undefined + if (shouldEmit != null) { + this.emit('close') + } + } + }) + + pipe( + this.outboundStream, + lp.encode(), + this._rawOutboundStream + ).catch((err: Error) => { + log.error(err) + }) + + // Only emit if the connection is new + if (_prevStream == null) { + this.emit('stream:outbound') + } + } + + /** + * Closes the open connection to peer + */ + close () { + // End the outbound stream + if (this.outboundStream != null) { + this.outboundStream.end() + } + // End the inbound stream + if (this.inboundStream != null) { + this._inboundAbortController.abort() + } + + this._rawOutboundStream = undefined + this.outboundStream = undefined + this._rawInboundStream = undefined + this.inboundStream = undefined + this.emit('close') + } +} diff --git a/packages/libp2p-pubsub/src/utils.ts b/packages/libp2p-pubsub/src/utils.ts new file mode 100644 index 000000000..c8ec3d479 --- /dev/null +++ b/packages/libp2p-pubsub/src/utils.ts @@ -0,0 +1,93 @@ +import { randomBytes } from 'iso-random-stream' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import PeerId from 'peer-id' +import { sha256 } from 'multiformats/hashes/sha2' +import type * as RPC from './message/rpc.js' +import type { Message } from 'libp2p-interfaces/pubsub' + +/** + * Generate a random sequence number + */ +export const randomSeqno = () => { + return randomBytes(8) +} + +/** + * Generate a message id, based on the `from` and `seqno` + */ +export const msgId = (from: Uint8Array | string, seqno: Uint8Array) => { + let fromBytes + + if (from instanceof Uint8Array) { + fromBytes = PeerId.createFromBytes(from).id + } else { + fromBytes = PeerId.parse(from).id + } + + const msgId = new Uint8Array(fromBytes.length + seqno.length) + msgId.set(fromBytes, 0) + msgId.set(seqno, fromBytes.length) + return msgId +} + +/** + * Generate a message id, based on message `data` + */ +export const noSignMsgId = (data: Uint8Array) => sha256.encode(data) + +/** + * Check if any member of the first set is also a member + * of the second set + */ +export const anyMatch = (a: Set | number[], b: Set | number[]) => { + let bHas + if (Array.isArray(b)) { + bHas = (val: number) => b.includes(val) + } else { + bHas = (val: number) => b.has(val) + } + + for (const val of a) { + if (bHas(val)) { + return true + } + } + + return false +} + +/** + * Make everything an array + */ +export const ensureArray = function (maybeArray: T | T[]) { + if (!Array.isArray(maybeArray)) { + return [maybeArray] + } + + return maybeArray +} + +/** + * Ensures `message.from` is base58 encoded + */ +export const normalizeInRpcMessage = (message: RPC.RPC.IMessage, peerId?: string) => { + // @ts-expect-error receivedFrom not yet defined + const m: NormalizedIMessage = Object.assign({}, message) + + if (peerId != null) { + m.receivedFrom = peerId + } + + return m +} + +export const normalizeOutRpcMessage = (message: Message) => { + const m: Message = Object.assign({}, message) + if (typeof message.from === 'string') { + m.from = uint8ArrayFromString(message.from, 'base58btc') + } + if (typeof message.data === 'string') { + m.data = uint8ArrayFromString(message.data) + } + return m +} diff --git a/packages/libp2p-pubsub/test/emit-self.spec.ts b/packages/libp2p-pubsub/test/emit-self.spec.ts new file mode 100644 index 000000000..9a892dd6c --- /dev/null +++ b/packages/libp2p-pubsub/test/emit-self.spec.ts @@ -0,0 +1,82 @@ +import { expect } from 'aegir/utils/chai.js' +import { + createPeerId, + mockRegistrar, + PubsubImplementation +} from './utils/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import delay from 'delay' + +const protocol = '/pubsub/1.0.0' +const topic = 'foo' +const data = uint8ArrayFromString('bar') +const shouldNotHappen = () => expect.fail() + +describe('emitSelf', () => { + let pubsub: PubsubImplementation + + describe('enabled', () => { + before(async () => { + const peerId = await createPeerId() + + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId, + registrar: mockRegistrar + }, + emitSelf: true + }) + }) + + before(() => { + pubsub.start() + pubsub.subscribe(topic) + }) + + after(() => { + pubsub.stop() + }) + + it('should emit to self on publish', async () => { + const promise = new Promise((resolve) => pubsub.once(topic, resolve)) + + await pubsub.publish(topic, data) + + return await promise + }) + }) + + describe('disabled', () => { + before(async () => { + const peerId = await createPeerId() + + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId, + registrar: mockRegistrar + }, + emitSelf: false + }) + }) + + before(() => { + pubsub.start() + pubsub.subscribe(topic) + }) + + after(() => { + pubsub.stop() + }) + + it('should not emit to self on publish', async () => { + pubsub.once(topic, () => shouldNotHappen) + + await pubsub.publish(topic, data) + + // Wait 1 second to guarantee that self is not noticed + await delay(1000) + }) + }) +}) diff --git a/packages/libp2p-pubsub/test/instance.spec.ts b/packages/libp2p-pubsub/test/instance.spec.ts new file mode 100644 index 000000000..6c29ab8f6 --- /dev/null +++ b/packages/libp2p-pubsub/test/instance.spec.ts @@ -0,0 +1,61 @@ +import { expect } from 'aegir/utils/chai.js' +import { PubsubBaseProtocol } from '../src/index.js' +import { + createPeerId, + mockRegistrar +} from './utils/index.js' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { Message } from 'libp2p-interfaces/pubsub' + +class PubsubProtocol extends PubsubBaseProtocol { + async _publish (message: Message): Promise { + throw new Error('Method not implemented.') + } +} + +describe('pubsub instance', () => { + let peerId: PeerId + + before(async () => { + peerId = await createPeerId() + }) + + it('should throw if no debugName is provided', () => { + expect(() => { + // @ts-expect-error incorrect constructor args + new PubsubProtocol() // eslint-disable-line no-new + }).to.throw() + }) + + it('should throw if no multicodec is provided', () => { + expect(() => { + // @ts-expect-error incorrect constructor args + new PubsubProtocol({ // eslint-disable-line no-new + debugName: 'pubsub' + }) + }).to.throw() + }) + + it('should throw if no libp2p is provided', () => { + expect(() => { + // @ts-expect-error incorrect constructor args + new PubsubProtocol({ // eslint-disable-line no-new + debugName: 'pubsub', + multicodecs: ['/pubsub/1.0.0'] + }) + }).to.throw() + }) + + it('should accept valid parameters', () => { + expect(() => { + new PubsubProtocol({ // eslint-disable-line no-new + debugName: 'pubsub', + multicodecs: ['/pubsub/1.0.0'], + libp2p: { + peerId: peerId, + registrar: mockRegistrar + } + }) + }).not.to.throw() + }) +}) diff --git a/packages/interfaces/test/pubsub/lifesycle.spec.js b/packages/libp2p-pubsub/test/lifesycle.spec.ts similarity index 57% rename from packages/interfaces/test/pubsub/lifesycle.spec.js rename to packages/libp2p-pubsub/test/lifesycle.spec.ts index 4da03440a..6e76b3ac6 100644 --- a/packages/interfaces/test/pubsub/lifesycle.spec.js +++ b/packages/libp2p-pubsub/test/lifesycle.spec.ts @@ -1,33 +1,38 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') - -const PubsubBaseImpl = require('../../src/pubsub') -const { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import { PubsubBaseProtocol } from '../src/index.js' +import { createPeerId, createMockRegistrar, PubsubImplementation, ConnectionPair -} = require('./utils') +} from './utils/index.js' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { Registrar } from 'libp2p-interfaces/registrar' +import type { Message } from 'libp2p-interfaces/pubsub' + +class PubsubProtocol extends PubsubBaseProtocol { + async _publish (message: Message): Promise { + throw new Error('Method not implemented.') + } +} describe('pubsub base lifecycle', () => { describe('should start and stop properly', () => { - let pubsub - let sinonMockRegistrar + let pubsub: PubsubProtocol + let sinonMockRegistrar: Partial beforeEach(async () => { const peerId = await createPeerId() sinonMockRegistrar = { handle: sinon.stub(), - register: sinon.stub(), + register: sinon.stub().returns(`id-${Math.random()}`), unregister: sinon.stub() } - pubsub = new PubsubBaseImpl({ + pubsub = new PubsubProtocol({ debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', + multicodecs: ['/pubsub/1.0.0'], libp2p: { peerId: peerId, registrar: sinonMockRegistrar @@ -43,49 +48,55 @@ describe('pubsub base lifecycle', () => { it('should be able to start and stop', async () => { await pubsub.start() - expect(sinonMockRegistrar.handle.calledOnce).to.be.true() - expect(sinonMockRegistrar.register.calledOnce).to.be.true() + expect(sinonMockRegistrar.handle).to.have.property('calledOnce', true) + expect(sinonMockRegistrar.register).to.have.property('calledOnce', true) await pubsub.stop() - expect(sinonMockRegistrar.unregister.calledOnce).to.be.true() + expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', true) }) it('starting should not throw if already started', async () => { await pubsub.start() await pubsub.start() - expect(sinonMockRegistrar.handle.calledOnce).to.be.true() - expect(sinonMockRegistrar.register.calledOnce).to.be.true() + expect(sinonMockRegistrar.handle).to.have.property('calledOnce', true) + expect(sinonMockRegistrar.register).to.have.property('calledOnce', true) await pubsub.stop() - expect(sinonMockRegistrar.unregister.calledOnce).to.be.true() + expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', true) }) it('stopping should not throw if not started', async () => { await pubsub.stop() - expect(sinonMockRegistrar.register.calledOnce).to.be.false() - expect(sinonMockRegistrar.unregister.calledOnce).to.be.false() + expect(sinonMockRegistrar.register).to.have.property('calledOnce', false) + expect(sinonMockRegistrar.unregister).to.have.property('calledOnce', false) }) }) describe('should be able to register two nodes', () => { const protocol = '/pubsub/1.0.0' - let pubsubA, pubsubB - let peerIdA, peerIdB - const registrarRecordA = {} - const registrarRecordB = {} + let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation + let peerIdA: PeerId, peerIdB: PeerId + const registrarRecordA = new Map() + const registrarRecordB = new Map() // mount pubsub beforeEach(async () => { peerIdA = await createPeerId() peerIdB = await createPeerId() - pubsubA = new PubsubImplementation(protocol, { - peerId: peerIdA, - registrar: createMockRegistrar(registrarRecordA) + pubsubA = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerIdA, + registrar: createMockRegistrar(registrarRecordA) + } }) - pubsubB = new PubsubImplementation(protocol, { - peerId: peerIdB, - registrar: createMockRegistrar(registrarRecordB) + pubsubB = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerIdB, + registrar: createMockRegistrar(registrarRecordB) + } }) }) @@ -94,22 +105,22 @@ describe('pubsub base lifecycle', () => { pubsubA.start() pubsubB.start() - expect(Object.keys(registrarRecordA)).to.have.lengthOf(1) - expect(Object.keys(registrarRecordB)).to.have.lengthOf(1) + expect(registrarRecordA).to.have.lengthOf(1) + expect(registrarRecordB).to.have.lengthOf(1) }) - afterEach(() => { + afterEach(async () => { sinon.restore() - return Promise.all([ + return await Promise.all([ pubsubA.stop(), pubsubB.stop() ]) }) it('should handle onConnect as expected', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler + const onConnectA = registrarRecordA.get(protocol).onConnect + const handlerB = registrarRecordB.get(protocol).handler // Notice peers of connection const [c0, c1] = ConnectionPair() @@ -128,8 +139,8 @@ describe('pubsub base lifecycle', () => { }) it('should use the latest connection if onConnect is called more than once', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler + const onConnectA = registrarRecordA.get(protocol).onConnect + const handlerB = registrarRecordB.get(protocol).handler // Notice peers of connection const [c0, c1] = ConnectionPair() @@ -147,19 +158,23 @@ describe('pubsub base lifecycle', () => { }) expect(c0.newStream).to.have.property('callCount', 1) + // @ts-expect-error _removePeer is a protected method sinon.spy(pubsubA, '_removePeer') sinon.spy(c2, 'newStream') await onConnectA(peerIdB, c2) expect(c2.newStream).to.have.property('callCount', 1) + + // @ts-expect-error _removePeer is a protected method expect(pubsubA._removePeer).to.have.property('callCount', 0) // Verify the first stream was closed + // @ts-expect-error .returnValues is a sinon property const { stream: firstStream } = await c0.newStream.returnValues[0] try { await firstStream.sink(['test']) - } catch (err) { + } catch (err: any) { expect(err).to.exist() return } @@ -167,8 +182,8 @@ describe('pubsub base lifecycle', () => { }) it('should handle newStream errors in onConnect', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler + const onConnectA = registrarRecordA.get(protocol).onConnect + const handlerB = registrarRecordB.get(protocol).handler // Notice peers of connection const [c0, c1] = ConnectionPair() @@ -188,10 +203,10 @@ describe('pubsub base lifecycle', () => { }) it('should handle onDisconnect as expected', async () => { - const onConnectA = registrarRecordA[protocol].onConnect - const onDisconnectA = registrarRecordA[protocol].onDisconnect - const handlerB = registrarRecordB[protocol].handler - const onDisconnectB = registrarRecordB[protocol].onDisconnect + const onConnectA = registrarRecordA.get(protocol).onConnect + const onDisconnectA = registrarRecordA.get(protocol).onDisconnect + const handlerB = registrarRecordB.get(protocol).handler + const onDisconnectB = registrarRecordB.get(protocol).onDisconnect // Notice peers of connection const [c0, c1] = ConnectionPair() @@ -214,7 +229,7 @@ describe('pubsub base lifecycle', () => { }) it('should handle onDisconnect for unknown peers', () => { - const onDisconnectA = registrarRecordA[protocol].onDisconnect + const onDisconnectA = registrarRecordA.get(protocol).onDisconnect expect(pubsubA.peers.size).to.be.eql(0) diff --git a/packages/interfaces/test/pubsub/message.spec.js b/packages/libp2p-pubsub/test/message.spec.ts similarity index 55% rename from packages/interfaces/test/pubsub/message.spec.js rename to packages/libp2p-pubsub/test/message.spec.ts index ecd66b89c..7e3e7eadd 100644 --- a/packages/interfaces/test/pubsub/message.spec.js +++ b/packages/libp2p-pubsub/test/message.spec.ts @@ -1,26 +1,34 @@ /* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const PubsubBaseImpl = require('../../src/pubsub') -const { SignaturePolicy } = require('../../src/pubsub/signature-policy') -const { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PubsubBaseProtocol } from '../src/index.js' +import { createPeerId, mockRegistrar -} = require('./utils') +} from './utils/index.js' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { Message } from 'libp2p-interfaces/pubsub' + +class PubsubProtocol extends PubsubBaseProtocol { + async _publish (message: Message): Promise { + throw new Error('Method not implemented') + } + + async buildMessage (message: Message) { + return await this._buildMessage(message) + } +} describe('pubsub base messages', () => { - let peerId - let pubsub + let peerId: PeerId + let pubsub: PubsubProtocol before(async () => { peerId = await createPeerId() - pubsub = new PubsubBaseImpl({ + pubsub = new PubsubProtocol({ debugName: 'pubsub', - multicodecs: '/pubsub/1.0.0', + multicodecs: ['/pubsub/1.0.0'], libp2p: { peerId: peerId, registrar: mockRegistrar @@ -34,28 +42,30 @@ describe('pubsub base messages', () => { it('_buildMessage normalizes and signs messages', async () => { const message = { + from: peerId.toBytes(), receivedFrom: peerId.toB58String(), data: uint8ArrayFromString('hello'), topicIDs: ['test-topic'] } - const signedMessage = await pubsub._buildMessage(message) + const signedMessage = await pubsub.buildMessage(message) await expect(pubsub.validate(signedMessage)).to.eventually.not.be.rejected() }) it('validate with StrictNoSign will reject a message with from, signature, key, seqno present', async () => { const message = { - receivedFrom: peerId.id, - data: 'hello', + from: peerId.toBytes(), + receivedFrom: peerId.toB58String(), + data: uint8ArrayFromString('hello'), topicIDs: ['test-topic'] } - sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictSign) + sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictSign') - const signedMessage = await pubsub._buildMessage(message) + const signedMessage = await pubsub.buildMessage(message) - sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictNoSign) + sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign') await expect(pubsub.validate(signedMessage)).to.eventually.be.rejected() delete signedMessage.from await expect(pubsub.validate(signedMessage)).to.eventually.be.rejected() @@ -69,21 +79,23 @@ describe('pubsub base messages', () => { it('validate with StrictNoSign will validate a message without a signature, key, and seqno', async () => { const message = { - receivedFrom: peerId.id, - data: 'hello', + from: peerId.toBytes(), + receivedFrom: peerId.toB58String(), + data: uint8ArrayFromString('hello'), topicIDs: ['test-topic'] } - sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictNoSign) + sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign') - const signedMessage = await pubsub._buildMessage(message) + const signedMessage = await pubsub.buildMessage(message) await expect(pubsub.validate(signedMessage)).to.eventually.not.be.rejected() }) it('validate with StrictSign requires a signature', async () => { const message = { - receivedFrom: peerId.id, - data: 'hello', + from: peerId.toBytes(), + receivedFrom: peerId.toB58String(), + data: uint8ArrayFromString('hello'), topicIDs: ['test-topic'] } diff --git a/packages/interfaces/test/pubsub/pubsub.spec.js b/packages/libp2p-pubsub/test/pubsub.spec.ts similarity index 66% rename from packages/interfaces/test/pubsub/pubsub.spec.js rename to packages/libp2p-pubsub/test/pubsub.spec.ts index 32f7e85c6..eadc674e9 100644 --- a/packages/interfaces/test/pubsub/pubsub.spec.js +++ b/packages/libp2p-pubsub/test/pubsub.spec.ts @@ -1,21 +1,17 @@ -/* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 6] */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') -const pWaitFor = require('p-wait-for') - -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const PeerStreams = require('../../src/pubsub/peer-streams') -const { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import pWaitFor from 'p-wait-for' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PeerStreams } from '../src/peer-streams.js' +import { createPeerId, createMockRegistrar, ConnectionPair, mockRegistrar, PubsubImplementation -} = require('./utils') +} from './utils/index.js' +import type { PeerId } from 'libp2p-interfaces/peer-id' const protocol = '/pubsub/1.0.0' const topic = 'test-topic' @@ -23,13 +19,16 @@ const message = uint8ArrayFromString('hello') describe('pubsub base implementation', () => { describe('publish', () => { - let pubsub + let pubsub: PubsubImplementation beforeEach(async () => { const peerId = await createPeerId() - pubsub = new PubsubImplementation(protocol, { - peerId: peerId, - registrar: mockRegistrar + pubsub = new PubsubImplementation({ + libp2p: { + peerId: peerId, + registrar: mockRegistrar + }, + multicodecs: [protocol] }) }) @@ -41,6 +40,7 @@ describe('pubsub base implementation', () => { pubsub.start() await pubsub.publish(topic, message) + // @ts-expect-error .callCount is a added by sinon expect(pubsub._publish.callCount).to.eql(1) }) @@ -51,24 +51,25 @@ describe('pubsub base implementation', () => { await pubsub.publish(topic, message) // Get the first message sent to _publish, and validate it + // @ts-expect-error .getCall is a added by sinon const signedMessage = pubsub._publish.getCall(0).lastArg - try { - await pubsub.validate(signedMessage) - } catch (e) { - expect.fail('validation should not throw') - } + + await expect(pubsub.validate(signedMessage)).to.eventually.be.undefined() }) }) describe('subscribe', () => { describe('basics', () => { - let pubsub + let pubsub: PubsubImplementation beforeEach(async () => { const peerId = await createPeerId() - pubsub = new PubsubImplementation(protocol, { - peerId: peerId, - registrar: mockRegistrar + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerId, + registrar: mockRegistrar + } }) pubsub.start() }) @@ -84,22 +85,28 @@ describe('pubsub base implementation', () => { }) describe('two nodes', () => { - let pubsubA, pubsubB - let peerIdA, peerIdB - const registrarRecordA = {} - const registrarRecordB = {} + let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation + let peerIdA: PeerId, peerIdB: PeerId + const registrarRecordA = new Map() + const registrarRecordB = new Map() beforeEach(async () => { peerIdA = await createPeerId() peerIdB = await createPeerId() - pubsubA = new PubsubImplementation(protocol, { - peerId: peerIdA, - registrar: createMockRegistrar(registrarRecordA) + pubsubA = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerIdA, + registrar: createMockRegistrar(registrarRecordA) + } }) - pubsubB = new PubsubImplementation(protocol, { - peerId: peerIdB, - registrar: createMockRegistrar(registrarRecordB) + pubsubB = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerIdB, + registrar: createMockRegistrar(registrarRecordB) + } }) }) @@ -108,8 +115,8 @@ describe('pubsub base implementation', () => { pubsubA.start() pubsubB.start() - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler + const onConnectA = registrarRecordA.get(protocol).onConnect + const handlerB = registrarRecordB.get(protocol).handler // Notice peers of connection const [c0, c1] = ConnectionPair() @@ -136,6 +143,7 @@ describe('pubsub base implementation', () => { pubsubA.subscribe(topic) // Should send subscriptions to a peer + // @ts-expect-error .callCount is a added by sinon expect(pubsubA._sendSubscriptions.callCount).to.eql(1) // Other peer should receive subscription message @@ -144,6 +152,8 @@ describe('pubsub base implementation', () => { return subscribers.length === 1 }) + + // @ts-expect-error .callCount is a added by sinon expect(pubsubB._processRpcSubOpt.callCount).to.eql(1) }) }) @@ -151,13 +161,16 @@ describe('pubsub base implementation', () => { describe('unsubscribe', () => { describe('basics', () => { - let pubsub + let pubsub: PubsubImplementation beforeEach(async () => { const peerId = await createPeerId() - pubsub = new PubsubImplementation(protocol, { - peerId: peerId, - registrar: mockRegistrar + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerId, + registrar: mockRegistrar + } }) pubsub.start() }) @@ -165,8 +178,8 @@ describe('pubsub base implementation', () => { afterEach(() => pubsub.stop()) it('should remove all subscriptions for a topic', () => { - pubsub.subscribe(topic, (msg) => {}) - pubsub.subscribe(topic, (msg) => {}) + pubsub.subscribe(topic) + pubsub.subscribe(topic) expect(pubsub.subscriptions.size).to.eql(1) @@ -177,22 +190,28 @@ describe('pubsub base implementation', () => { }) describe('two nodes', () => { - let pubsubA, pubsubB - let peerIdA, peerIdB - const registrarRecordA = {} - const registrarRecordB = {} + let pubsubA: PubsubImplementation, pubsubB: PubsubImplementation + let peerIdA: PeerId, peerIdB: PeerId + const registrarRecordA = new Map() + const registrarRecordB = new Map() beforeEach(async () => { peerIdA = await createPeerId() peerIdB = await createPeerId() - pubsubA = new PubsubImplementation(protocol, { - peerId: peerIdA, - registrar: createMockRegistrar(registrarRecordA) + pubsubA = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerIdA, + registrar: createMockRegistrar(registrarRecordA) + } }) - pubsubB = new PubsubImplementation(protocol, { - peerId: peerIdB, - registrar: createMockRegistrar(registrarRecordB) + pubsubB = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerIdB, + registrar: createMockRegistrar(registrarRecordB) + } }) }) @@ -201,8 +220,8 @@ describe('pubsub base implementation', () => { pubsubA.start() pubsubB.start() - const onConnectA = registrarRecordA[protocol].onConnect - const handlerB = registrarRecordB[protocol].handler + const onConnectA = registrarRecordA.get(protocol).onConnect + const handlerB = registrarRecordB.get(protocol).handler // Notice peers of connection const [c0, c1] = ConnectionPair() @@ -228,6 +247,7 @@ describe('pubsub base implementation', () => { pubsubA.subscribe(topic) // Should send subscriptions to a peer + // @ts-expect-error .callCount is a property added by sinon expect(pubsubA._sendSubscriptions.callCount).to.eql(1) // Other peer should receive subscription message @@ -236,11 +256,15 @@ describe('pubsub base implementation', () => { return subscribers.length === 1 }) + + // @ts-expect-error .callCount is a property added by sinon expect(pubsubB._processRpcSubOpt.callCount).to.eql(1) // Unsubscribe pubsubA.unsubscribe(topic) + // Should send subscriptions to a peer + // @ts-expect-error .callCount is a property added by sinon expect(pubsubA._sendSubscriptions.callCount).to.eql(2) // Other peer should receive subscription message @@ -249,6 +273,8 @@ describe('pubsub base implementation', () => { return subscribers.length === 0 }) + + // @ts-expect-error .callCount is a property added by sinon expect(pubsubB._processRpcSubOpt.callCount).to.eql(2) }) @@ -260,20 +286,24 @@ describe('pubsub base implementation', () => { pubsubA.unsubscribe(topic) // Should send subscriptions to a peer + // @ts-expect-error .callCount is a property added by sinon expect(pubsubA._sendSubscriptions.callCount).to.eql(0) }) }) }) describe('getTopics', () => { - let peerId - let pubsub + let peerId: PeerId + let pubsub: PubsubImplementation beforeEach(async () => { peerId = await createPeerId() - pubsub = new PubsubImplementation(protocol, { - peerId: peerId, - registrar: mockRegistrar + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerId, + registrar: mockRegistrar + } }) pubsub.start() }) @@ -293,14 +323,17 @@ describe('pubsub base implementation', () => { }) describe('getSubscribers', () => { - let peerId - let pubsub + let peerId: PeerId + let pubsub: PubsubImplementation beforeEach(async () => { peerId = await createPeerId() - pubsub = new PubsubImplementation(protocol, { - peerId: peerId, - registrar: mockRegistrar + pubsub = new PubsubImplementation({ + multicodecs: [protocol], + libp2p: { + peerId: peerId, + registrar: mockRegistrar + } }) }) @@ -311,7 +344,7 @@ describe('pubsub base implementation', () => { try { pubsub.getSubscribers(topic) - } catch (err) { + } catch (err: any) { expect(err).to.exist() expect(err.code).to.eql('ERR_NOT_STARTED_YET') return @@ -324,8 +357,9 @@ describe('pubsub base implementation', () => { pubsub.start() try { + // @ts-expect-error invalid params pubsub.getSubscribers() - } catch (err) { + } catch (err: any) { expect(err).to.exist() expect(err.code).to.eql('ERR_NOT_VALID_TOPIC') return @@ -343,7 +377,7 @@ describe('pubsub base implementation', () => { expect(peersSubscribed).to.be.empty() // Set mock peer subscribed - const peer = new PeerStreams({ id: peerId }) + const peer = new PeerStreams({ id: peerId, protocol: 'a-protocol' }) const id = peer.id.toB58String() pubsub.topics.set(topic, new Set([id])) diff --git a/packages/interfaces/test/pubsub/sign.spec.js b/packages/libp2p-pubsub/test/sign.spec.ts similarity index 77% rename from packages/interfaces/test/pubsub/sign.spec.js rename to packages/libp2p-pubsub/test/sign.spec.ts index e160743d9..779e276f9 100644 --- a/packages/interfaces/test/pubsub/sign.spec.js +++ b/packages/libp2p-pubsub/test/sign.spec.ts @@ -1,23 +1,19 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const { concat: uint8ArrayConcat } = require('uint8arrays/concat') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const { RPC } = require('../../src/pubsub/message/rpc') -const { +import { expect } from 'aegir/utils/chai.js' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { RPC } from '../src/message/rpc.js' +import { signMessage, SignPrefix, verifySignature -} = require('../../src/pubsub/message/sign') -const PeerId = require('peer-id') -const { randomSeqno } = require('../../src/pubsub/utils') +} from '../src/message/sign.js' +import PeerId from 'peer-id' +import { randomSeqno } from '../src/utils.js' +import type { Message } from 'libp2p-interfaces/pubsub' describe('message signing', () => { - /** @type {PeerId} */ - let peerId + let peerId: PeerId + before(async () => { peerId = await PeerId.create({ bits: 1024 @@ -25,8 +21,9 @@ describe('message signing', () => { }) it('should be able to sign and verify a message', async () => { - const message = { + const message: Message = { from: peerId.toBytes(), + receivedFrom: peerId.toB58String(), data: uint8ArrayFromString('hello'), seqno: randomSeqno(), topicIDs: ['test-topic'] @@ -44,7 +41,7 @@ describe('message signing', () => { // Verify the signature const verified = await verifySignature({ ...signedMessage, - from: peerId.toB58String() + from: peerId.toBytes() }) expect(verified).to.eql(true) }) @@ -52,8 +49,9 @@ describe('message signing', () => { it('should be able to extract the public key from an inlined key', async () => { const secPeerId = await PeerId.create({ keyType: 'secp256k1' }) - const message = { + const message: Message = { from: secPeerId.toBytes(), + receivedFrom: secPeerId.toB58String(), data: uint8ArrayFromString('hello'), seqno: randomSeqno(), topicIDs: ['test-topic'] @@ -71,14 +69,15 @@ describe('message signing', () => { // Verify the signature const verified = await verifySignature({ ...signedMessage, - from: secPeerId.toB58String() + from: secPeerId.toBytes() }) expect(verified).to.eql(true) }) it('should be able to extract the public key from the message', async () => { - const message = { + const message: Message = { from: peerId.toBytes(), + receivedFrom: peerId.toB58String(), data: uint8ArrayFromString('hello'), seqno: randomSeqno(), topicIDs: ['test-topic'] @@ -96,7 +95,7 @@ describe('message signing', () => { // Verify the signature const verified = await verifySignature({ ...signedMessage, - from: peerId.toB58String() + from: peerId.toBytes() }) expect(verified).to.eql(true) }) diff --git a/packages/interfaces/test/pubsub/topic-validators.spec.js b/packages/libp2p-pubsub/test/topic-validators.spec.ts similarity index 55% rename from packages/interfaces/test/pubsub/topic-validators.spec.js rename to packages/libp2p-pubsub/test/topic-validators.spec.ts index 75cfb2fe2..d19d0900a 100644 --- a/packages/interfaces/test/pubsub/topic-validators.spec.js +++ b/packages/libp2p-pubsub/test/topic-validators.spec.ts @@ -1,37 +1,32 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const sinon = require('sinon') -const pWaitFor = require('p-wait-for') -const errCode = require('err-code') - -const PeerId = require('peer-id') -const { equals: uint8ArrayEquals } = require('uint8arrays/equals') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') - -const PeerStreams = require('../../src/pubsub/peer-streams') -const { SignaturePolicy } = require('../../src/pubsub/signature-policy') - -const { +import { expect } from 'aegir/utils/chai.js' +import sinon from 'sinon' +import pWaitFor from 'p-wait-for' +import errCode from 'err-code' +import PeerIdFactory from 'peer-id' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PeerStreams } from '../src/peer-streams.js' +import { createPeerId, mockRegistrar, PubsubImplementation -} = require('./utils') +} from './utils/index.js' const protocol = '/pubsub/1.0.0' describe('topic validators', () => { - let pubsub + let pubsub: PubsubImplementation beforeEach(async () => { const peerId = await createPeerId() - pubsub = new PubsubImplementation(protocol, { - peerId: peerId, - registrar: mockRegistrar - }, { - globalSignaturePolicy: SignaturePolicy.StrictNoSign + pubsub = new PubsubImplementation({ + libp2p: { + peerId: peerId, + registrar: mockRegistrar + }, + multicodecs: [protocol], + globalSignaturePolicy: 'StrictNoSign' }) pubsub.start() @@ -44,12 +39,13 @@ describe('topic validators', () => { it('should filter messages by topic validator', async () => { // use _publish.callCount() to see if a message is valid or not sinon.spy(pubsub, '_publish') + // @ts-expect-error not all fields are implemented in return value sinon.stub(pubsub.peers, 'get').returns({}) const filteredTopic = 't' - const peer = new PeerStreams({ id: await PeerId.create() }) + const peer = new PeerStreams({ id: await PeerIdFactory.create(), protocol: 'a-protocol' }) // Set a trivial topic validator - pubsub.topicValidators.set(filteredTopic, (topic, message) => { + pubsub.topicValidators.set(filteredTopic, async (topic, message) => { if (!uint8ArrayEquals(message.data, uint8ArrayFromString('a message'))) { throw errCode(new Error(), 'ERR_TOPIC_VALIDATOR_REJECT') } @@ -61,13 +57,15 @@ describe('topic validators', () => { msgs: [{ data: uint8ArrayFromString('a message'), topicIDs: [filteredTopic] - }] + }], + toJSON: () => ({}) } // process valid message pubsub.subscribe(filteredTopic) - pubsub._processRpc(peer.id.toB58String(), peer, validRpc) + void pubsub._processRpc(peer.id.toB58String(), peer, validRpc) + // @ts-expect-error .callCount is a property added by sinon await pWaitFor(() => pubsub._publish.callCount === 1) // invalid case @@ -76,11 +74,14 @@ describe('topic validators', () => { msgs: [{ data: uint8ArrayFromString('a different message'), topicIDs: [filteredTopic] - }] + }], + toJSON: () => ({}) } // process invalid message - pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc) + void pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc) + + // @ts-expect-error .callCount is a property added by sinon expect(pubsub._publish.callCount).to.eql(1) // remove topic validator @@ -92,13 +93,15 @@ describe('topic validators', () => { msgs: [{ data: uint8ArrayFromString('a different message'), topicIDs: [filteredTopic] - }] + }], + toJSON: () => ({}) } // process previously invalid message, now is valid - pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc2) + void pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc2) pubsub.unsubscribe(filteredTopic) + // @ts-expect-error .callCount is a property added by sinon await pWaitFor(() => pubsub._publish.callCount === 2) }) }) diff --git a/packages/libp2p-pubsub/test/utils.spec.ts b/packages/libp2p-pubsub/test/utils.spec.ts new file mode 100644 index 000000000..78338c0af --- /dev/null +++ b/packages/libp2p-pubsub/test/utils.spec.ts @@ -0,0 +1,58 @@ +import { expect } from 'aegir/utils/chai.js' +import * as utils from '../src/utils.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +describe('utils', () => { + it('randomSeqno', () => { + const first = utils.randomSeqno() + const second = utils.randomSeqno() + + expect(first).to.have.length(8) + expect(second).to.have.length(8) + expect(first).to.not.eql(second) + }) + + it('msgId should not generate same ID for two different Uint8Arrays', () => { + const peerId = 'QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22' + const msgId0 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfde', 'base16')) + const msgId1 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfe0', 'base16')) + expect(msgId0).to.not.deep.equal(msgId1) + }) + + it('anyMatch', () => { + [ + { a: [1, 2, 3], b: [4, 5, 6], result: false }, + { a: [1, 2], b: [1, 2], result: true }, + { a: [1, 2, 3], b: [4, 5, 1], result: true }, + { a: [5, 6, 1], b: [1, 2, 3], result: true }, + { a: [], b: [], result: false }, + { a: [1], b: [2], result: false } + ].forEach((test) => { + expect(utils.anyMatch(new Set(test.a), new Set(test.b))).to.equal(test.result) + expect(utils.anyMatch(new Set(test.a), test.b)).to.equal(test.result) + }) + }) + + it('ensureArray', () => { + expect(utils.ensureArray('hello')).to.be.eql(['hello']) + expect(utils.ensureArray([1, 2])).to.be.eql([1, 2]) + }) + + it('converts an OUT msg.from to binary', () => { + const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16') + const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM' + const m = [{ + from: binaryId + }, { + from: stringId + }] + const expected = [ + { from: binaryId }, + { from: binaryId } + ] + for (let i = 0; i < m.length; i++) { + // @ts-expect-error some Message fields are missing from m + expect(utils.normalizeOutRpcMessage(m[i])).to.deep.equal(expected[i]) + } + }) +}) diff --git a/packages/libp2p-pubsub/test/utils/index.ts b/packages/libp2p-pubsub/test/utils/index.ts new file mode 100644 index 000000000..b5ffdfa77 --- /dev/null +++ b/packages/libp2p-pubsub/test/utils/index.ts @@ -0,0 +1,103 @@ +// @ts-expect-error no types +import DuplexPair from 'it-pair/duplex.js' +import PeerIdFactory from 'peer-id' +import { PubsubBaseProtocol } from '../../src/index.js' +import { RPC, IRPC } from '../../src/message/rpc.js' +import type { Registrar } from 'libp2p-interfaces/registrar' +import type { PeerId } from 'libp2p-interfaces/peer-id' + +export const createPeerId = async () => { + const peerId = await PeerIdFactory.create({ bits: 1024 }) + + return peerId +} + +export class PubsubImplementation extends PubsubBaseProtocol { + async _publish () { + // ... + } + + _decodeRpc (bytes: Uint8Array) { + return RPC.decode(bytes) + } + + _encodeRpc (rpc: IRPC) { + return RPC.encode(rpc).finish() + } +} + +export const mockRegistrar = { + handle: () => {}, + register: () => {}, + unregister: () => {} +} + +export const createMockRegistrar = (registrarRecord: Map>) => { + const registrar: Registrar = { + handle: (multicodecs: string[], handler) => { + const rec = registrarRecord.get(multicodecs[0]) ?? {} + + registrarRecord.set(multicodecs[0], { + ...rec, + handler + }) + }, + register: (topology) => { + const { multicodecs } = topology + const rec = registrarRecord.get(multicodecs[0]) ?? {} + + registrarRecord.set(multicodecs[0], { + ...rec, + onConnect: topology._onConnect, + onDisconnect: topology._onDisconnect + }) + + return multicodecs[0] + }, + unregister: (id: string) => { + registrarRecord.delete(id) + }, + + getConnection (peerId: PeerId) { + throw new Error('Not implemented') + }, + + peerStore: { + on: () => { + throw new Error('Not implemented') + }, + protoBook: { + get: () => { + throw new Error('Not implemented') + } + }, + peers: new Map(), + get: (peerId) => { + throw new Error('Not implemented') + } + }, + + connectionManager: { + on: () => { + throw new Error('Not implemented') + } + } + } + + return registrar +} + +export const ConnectionPair = () => { + const [d0, d1] = DuplexPair() + + return [ + { + stream: d0, + newStream: async () => await Promise.resolve({ stream: d0 }) + }, + { + stream: d1, + newStream: async () => await Promise.resolve({ stream: d1 }) + } + ] +} diff --git a/packages/libp2p-pubsub/tsconfig.json b/packages/libp2p-pubsub/tsconfig.json new file mode 100644 index 000000000..6527c2e56 --- /dev/null +++ b/packages/libp2p-pubsub/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "exclude": [ + "src/message/rpc.js", // exclude generated file + "src/message/topic-descriptor.js" // exclude generated file + ], + "references": [ + { + "path": "../libp2p-interfaces" + }, + { + "path": "../libp2p-topology" + } + ] +} diff --git a/packages/libp2p-topology/package.json b/packages/libp2p-topology/package.json new file mode 100644 index 000000000..4d271ba7f --- /dev/null +++ b/packages/libp2p-topology/package.json @@ -0,0 +1,59 @@ +{ + "name": "libp2p-topology", + "version": "0.0.1", + "description": "libp2p network topology", + "type": "module", + "files": [ + "src", + "dist" + ], + "typesVersions": { + "*": { + "*": [ + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "eslintConfig": { + "extends": "ipfs" + }, + "scripts": { + "lint": "aegir lint", + "dep-check": "aegir dep-check dist/src/**/*.js", + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-interfaces.git" + }, + "keywords": [ + "libp2p", + "interface" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-interfaces/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-topography#readme#readme", + "dependencies": { + "err-code": "^3.0.1", + "libp2p-interfaces": "^1.2.0", + "multiaddr": "^10.0.1", + "peer-id": "^0.15.3" + }, + "devDependencies": { + "aegir": "^36.0.0" + }, + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "./multicodec-topology": { + "import": "./dist/src/multicodec-topology.js", + "types": "./dist/src/multicodec-topology.d.ts" + } + } +} diff --git a/packages/libp2p-topology/src/index.ts b/packages/libp2p-topology/src/index.ts new file mode 100644 index 000000000..b465a5dc6 --- /dev/null +++ b/packages/libp2p-topology/src/index.ts @@ -0,0 +1,59 @@ +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { TopologyOptions, onConnectHandler, onDisconnectHandler } from 'libp2p-interfaces/topology' +import type { Registrar } from 'libp2p-interfaces/registrar' + +const noop = () => {} +const topologySymbol = Symbol.for('@libp2p/js-interfaces/topology') + +export class Topology { + public min: number + public max: number + + /** + * Set of peers that support the protocol + */ + public peers: Set + + protected _onConnect: onConnectHandler + protected _onDisconnect: onDisconnectHandler + protected _registrar: Registrar | undefined + + constructor (options: TopologyOptions) { + this.min = options.min ?? 0 + this.max = options.max ?? Infinity + this.peers = new Set() + + this._onConnect = options.handlers?.onConnect == null ? noop : options.handlers?.onConnect + this._onDisconnect = options.handlers?.onDisconnect == null ? noop : options.handlers?.onDisconnect + } + + get [Symbol.toStringTag] () { + return 'Topology' + } + + get [topologySymbol] () { + return true + } + + /** + * Checks if the given value is a Topology instance + */ + static isTopology (other: any) { + return topologySymbol in other + } + + set registrar (registrar: Registrar | undefined) { + this._registrar = registrar + } + + get registrar () { + return this._registrar + } + + /** + * Notify about peer disconnected event + */ + disconnect (peerId: PeerId) { + this._onDisconnect(peerId) + } +} diff --git a/packages/libp2p-topology/src/multicodec-topology.ts b/packages/libp2p-topology/src/multicodec-topology.ts new file mode 100644 index 000000000..986433bfe --- /dev/null +++ b/packages/libp2p-topology/src/multicodec-topology.ts @@ -0,0 +1,126 @@ +import { Topology } from './index.js' +import type { PeerId } from 'libp2p-interfaces/peer-id' +import type { PeerData } from 'libp2p-interfaces/peer-data' +import type { Connection } from 'libp2p-interfaces/connection' +import type { Registrar } from 'libp2p-interfaces/registrar' +import type { MulticodecTopologyOptions } from 'libp2p-interfaces/topology' + +interface ChangeProtocolsEvent { + peerId: PeerId + protocols: string[] +} + +const multicodecTopologySymbol = Symbol.for('@libp2p/js-interfaces/topology/multicodec-topology') + +export class MulticodecTopology extends Topology { + public readonly multicodecs: string[] + + constructor (options: MulticodecTopologyOptions) { + super(options) + + this.multicodecs = options.multicodecs + } + + get [Symbol.toStringTag] () { + return 'Topology' + } + + get [multicodecTopologySymbol] () { + return true + } + + /** + * Checks if the given value is a `MulticodecTopology` instance. + */ + static isMulticodecTopology (other: any) { + return Boolean(multicodecTopologySymbol in other) + } + + set registrar (registrar: Registrar | undefined) { + if (registrar == null) { + return + } + + this._registrar = registrar + + registrar.peerStore.on('change:protocols', this._onProtocolChange.bind(this)) + registrar.connectionManager.on('peer:connect', this._onPeerConnect.bind(this)) + + // Update topology peers + this._updatePeers(registrar.peerStore.peers.values()) + } + + get registrar () { + return this._registrar + } + + /** + * Update topology + * + * @param peerDatas + */ + _updatePeers (peerDatas: Iterable) { + for (const { id, protocols } of peerDatas) { + if (this.multicodecs.filter(multicodec => protocols.includes(multicodec)).length > 0) { + // Add the peer regardless of whether or not there is currently a connection + this.peers.add(id.toB58String()) + // If there is a connection, call _onConnect + if (this._registrar != null) { + const connection = this._registrar.getConnection(id) + ;(connection != null) && this._onConnect(id, connection) + } + } else { + // Remove any peers we might be tracking that are no longer of value to us + this.peers.delete(id.toB58String()) + } + } + } + + /** + * Check if a new peer support the multicodecs for this topology + */ + _onProtocolChange (event: ChangeProtocolsEvent) { + if (this._registrar == null) { + return + } + + const { peerId, protocols } = event + const hadPeer = this.peers.has(peerId.toB58String()) + const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol)) + + // Not supporting the protocol any more? + if (hadPeer && hasProtocol.length === 0) { + this._onDisconnect(peerId) + } + + // New to protocol support + for (const protocol of protocols) { + if (this.multicodecs.includes(protocol)) { + const peerData = this._registrar.peerStore.get(peerId) + this._updatePeers([peerData]) + return + } + } + } + + /** + * Verify if a new connected peer has a topology multicodec and call _onConnect + */ + _onPeerConnect (connection: Connection) { + if (this._registrar == null) { + return + } + + const peerId = connection.remotePeer + const protocols = this._registrar.peerStore.protoBook.get(peerId) + + if (protocols == null) { + return + } + + if (this.multicodecs.find(multicodec => protocols.includes(multicodec)) != null) { + this.peers.add(peerId.toB58String()) + this._onConnect(peerId, connection) + } + } +} diff --git a/packages/libp2p-topology/tsconfig.json b/packages/libp2p-topology/tsconfig.json new file mode 100644 index 000000000..4b68f4ffa --- /dev/null +++ b/packages/libp2p-topology/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../libp2p-interfaces" + } + ] +}