'use strict' /* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 6] */ const { expect } = require('aegir/utils/chai') const { Buffer } = require('buffer') const { Multiaddr } = require('multiaddr') const arrayEquals = require('libp2p-utils/src/array-equals') const addressSort = require('libp2p-utils/src/address-sort') const PeerId = require('peer-id') const pDefer = require('p-defer') const { MemoryDatastore } = require('datastore-core/memory') const PeerStore = require('../../src/peer-store') const Envelope = require('../../src/record/envelope') const PeerRecord = require('../../src/record/peer-record') const { mockConnectionGater } = require('../utils/mock-connection-gater') const peerUtils = require('../utils/creators/peer') const { codes: { ERR_INVALID_PARAMETERS } } = require('../../src/errors') /** * @typedef {import('../../src/peer-store/types').PeerStore} PeerStore * @typedef {import('../../src/peer-store/types').AddressBook} AddressBook */ const addr1 = new Multiaddr('/ip4/127.0.0.1/tcp/8000') const addr2 = new Multiaddr('/ip4/20.0.0.1/tcp/8001') const addr3 = new Multiaddr('/ip4/127.0.0.1/tcp/8002') describe('addressBook', () => { const connectionGater = mockConnectionGater() let peerId before(async () => { [peerId] = await peerUtils.createPeerId() }) describe('addressBook.set', () => { /** @type {PeerStore} */ let peerStore /** @type {AddressBook} */ let ab beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) afterEach(() => { peerStore.removeAllListeners() }) it('throws invalid parameters error if invalid PeerId is provided', async () => { try { await ab.set('invalid peerId') } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid peerId should throw error') }) it('throws invalid parameters error if no addresses provided', async () => { try { await ab.set(peerId) } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('no addresses should throw error') }) it('throws invalid parameters error if invalid multiaddrs are provided', async () => { try { await ab.set(peerId, ['invalid multiaddr']) } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid multiaddrs should throw error') }) it('replaces the stored content by default and emit change event', async () => { const defer = pDefer() const supportedMultiaddrs = [addr1, addr2] peerStore.once('change:multiaddrs', ({ peerId, multiaddrs }) => { expect(peerId).to.exist() expect(multiaddrs).to.eql(supportedMultiaddrs) defer.resolve() }) await ab.set(peerId, supportedMultiaddrs) const addresses = await ab.get(peerId) const multiaddrs = addresses.map((mi) => mi.multiaddr) expect(multiaddrs).to.have.deep.members(supportedMultiaddrs) return defer.promise }) it('emits on set if not storing the exact same content', async () => { const defer = pDefer() const supportedMultiaddrsA = [addr1, addr2] const supportedMultiaddrsB = [addr2] let changeCounter = 0 peerStore.on('change:multiaddrs', () => { changeCounter++ if (changeCounter > 1) { defer.resolve() } }) // set 1 await ab.set(peerId, supportedMultiaddrsA) // set 2 (same content) await ab.set(peerId, supportedMultiaddrsB) const addresses = await ab.get(peerId) const multiaddrs = addresses.map((mi) => mi.multiaddr) expect(multiaddrs).to.have.deep.members(supportedMultiaddrsB) await defer.promise }) it('does not emit on set if it is storing the exact same content', async () => { const defer = pDefer() const supportedMultiaddrs = [addr1, addr2] let changeCounter = 0 peerStore.on('change:multiaddrs', () => { changeCounter++ if (changeCounter > 1) { defer.reject() } }) // set 1 await ab.set(peerId, supportedMultiaddrs) // set 2 (same content) await ab.set(peerId, supportedMultiaddrs) // Wait 50ms for incorrect second event setTimeout(() => { defer.resolve() }, 50) await defer.promise }) }) describe('addressBook.add', () => { /** @type {PeerStore} */ let peerStore /** @type {AddressBook} */ let ab beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) afterEach(() => { peerStore.removeAllListeners() }) it('throws invalid parameters error if invalid PeerId is provided', async () => { try { await ab.add('invalid peerId') } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid peerId should throw error') }) it('throws invalid parameters error if no addresses provided', async () => { try { await ab.add(peerId) } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('no addresses provided should throw error') }) it('throws invalid parameters error if invalid multiaddrs are provided', async () => { try { await ab.add(peerId, ['invalid multiaddr']) } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid multiaddr should throw error') }) it('does not emit event if no addresses are added', async () => { const defer = pDefer() peerStore.on('peer', () => { defer.reject() }) await ab.add(peerId, []) // Wait 50ms for incorrect second event setTimeout(() => { defer.resolve() }, 50) await defer.promise }) it('adds the new content and emits change event', async () => { const defer = pDefer() const supportedMultiaddrsA = [addr1, addr2] const supportedMultiaddrsB = [addr3] const finalMultiaddrs = supportedMultiaddrsA.concat(supportedMultiaddrsB) let changeTrigger = 2 peerStore.on('change:multiaddrs', ({ multiaddrs }) => { changeTrigger-- if (changeTrigger === 0 && arrayEquals(multiaddrs, finalMultiaddrs)) { defer.resolve() } }) // Replace await ab.set(peerId, supportedMultiaddrsA) let addresses = await ab.get(peerId) let multiaddrs = addresses.map((mi) => mi.multiaddr) expect(multiaddrs).to.have.deep.members(supportedMultiaddrsA) // Add await ab.add(peerId, supportedMultiaddrsB) addresses = await ab.get(peerId) multiaddrs = addresses.map((mi) => mi.multiaddr) expect(multiaddrs).to.have.deep.members(finalMultiaddrs) return defer.promise }) it('emits on add if the content to add not exists', async () => { const defer = pDefer() const supportedMultiaddrsA = [addr1] const supportedMultiaddrsB = [addr2] const finalMultiaddrs = supportedMultiaddrsA.concat(supportedMultiaddrsB) let changeCounter = 0 peerStore.on('change:multiaddrs', () => { changeCounter++ if (changeCounter > 1) { defer.resolve() } }) // set 1 await ab.set(peerId, supportedMultiaddrsA) // set 2 (content already existing) await ab.add(peerId, supportedMultiaddrsB) const addresses = await ab.get(peerId) const multiaddrs = addresses.map((mi) => mi.multiaddr) expect(multiaddrs).to.have.deep.members(finalMultiaddrs) await defer.promise }) it('does not emit on add if the content to add already exists', async () => { const defer = pDefer() const supportedMultiaddrsA = [addr1, addr2] const supportedMultiaddrsB = [addr2] let changeCounter = 0 peerStore.on('change:multiaddrs', () => { changeCounter++ if (changeCounter > 1) { defer.reject() } }) // set 1 await ab.set(peerId, supportedMultiaddrsA) // set 2 (content already existing) await ab.add(peerId, supportedMultiaddrsB) // Wait 50ms for incorrect second event setTimeout(() => { defer.resolve() }, 50) await defer.promise }) it('does not add replicated content', async () => { // set 1 await ab.set(peerId, [addr1, addr1]) const addresses = await ab.get(peerId) expect(addresses).to.have.lengthOf(1) }) }) describe('addressBook.get', () => { /** @type {PeerStore} */ let peerStore /** @type {AddressBook} */ let ab beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) it('throws invalid parameters error if invalid PeerId is provided', async () => { try { await ab.get('invalid peerId') } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid peerId should throw error') }) it('returns empty if no multiaddrs are known for the provided peer', async () => { const addresses = await ab.get(peerId) expect(addresses).to.be.empty() }) it('returns the multiaddrs stored', async () => { const supportedMultiaddrs = [addr1, addr2] await ab.set(peerId, supportedMultiaddrs) const addresses = await ab.get(peerId) const multiaddrs = addresses.map((mi) => mi.multiaddr) expect(multiaddrs).to.have.deep.members(supportedMultiaddrs) }) }) describe('addressBook.getMultiaddrsForPeer', () => { /** @type {PeerStore} */ let peerStore /** @type {AddressBook} */ let ab beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) it('throws invalid parameters error if invalid PeerId is provided', async () => { try { await ab.getMultiaddrsForPeer('invalid peerId') } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid peerId should throw error') }) it('returns empty if no multiaddrs are known for the provided peer', async () => { const addresses = await ab.getMultiaddrsForPeer(peerId) expect(addresses).to.be.empty() }) it('returns the multiaddrs stored', async () => { const supportedMultiaddrs = [addr1, addr2] await ab.set(peerId, supportedMultiaddrs) const multiaddrs = await ab.getMultiaddrsForPeer(peerId) multiaddrs.forEach((m) => { expect(m.getPeerId()).to.equal(peerId.toB58String()) }) }) it('can sort multiaddrs providing a sorter', async () => { const supportedMultiaddrs = [addr1, addr2] await ab.set(peerId, supportedMultiaddrs) const multiaddrs = await ab.getMultiaddrsForPeer(peerId, addressSort.publicAddressesFirst) const sortedAddresses = addressSort.publicAddressesFirst(supportedMultiaddrs.map((m) => ({ multiaddr: m }))) multiaddrs.forEach((m, index) => { expect(m.equals(sortedAddresses[index].multiaddr)) }) }) }) describe('addressBook.delete', () => { /** @type {PeerStore} */ let peerStore /** @type {AddressBook} */ let ab beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) it('throws invalid parameters error if invalid PeerId is provided', async () => { try { await ab.delete('invalid peerId') } catch (/** @type {any} */ err) { expect(err.code).to.equal(ERR_INVALID_PARAMETERS) return } throw new Error('invalid peerId should throw error') }) it('does not emit an event if no records exist for the peer', async () => { const defer = pDefer() peerStore.on('change:multiaddrs', () => { defer.reject() }) await ab.delete(peerId) // Wait 50ms for incorrect invalid event setTimeout(() => { defer.resolve() }, 50) return defer.promise }) it('emits an event if the record exists', async () => { const defer = pDefer() const supportedMultiaddrs = [addr1, addr2] await ab.set(peerId, supportedMultiaddrs) // Listen after set peerStore.on('change:multiaddrs', ({ multiaddrs }) => { expect(multiaddrs.length).to.eql(0) defer.resolve() }) await ab.delete(peerId) return defer.promise }) }) describe('certified records', () => { /** @type {PeerStore} */ let peerStore /** @type {AddressBook} */ let ab describe('consumes a valid peer record and stores its data', () => { beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) it('no previous data in AddressBook', async () => { const multiaddrs = [addr1, addr2] const peerRecord = new PeerRecord({ peerId, multiaddrs }) const envelope = await Envelope.seal(peerRecord, peerId) // consume peer record const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(true) // Validate AddressBook addresses const addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(multiaddrs.length) addrs.forEach((addr, index) => { expect(addr.isCertified).to.eql(true) expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) }) }) it('emits change:multiaddrs event when adding multiaddrs', async () => { const defer = pDefer() const multiaddrs = [addr1, addr2] const peerRecord = new PeerRecord({ peerId, multiaddrs }) const envelope = await Envelope.seal(peerRecord, peerId) peerStore.once('change:multiaddrs', ({ peerId, multiaddrs }) => { expect(peerId).to.exist() expect(multiaddrs).to.eql(multiaddrs) defer.resolve() }) // consume peer record const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(true) return defer.promise }) it('emits change:multiaddrs event with same data currently in AddressBook (not certified)', async () => { const defer = pDefer() const multiaddrs = [addr1, addr2] // Set addressBook data await ab.set(peerId, multiaddrs) // Validate data exists, but not certified let addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(multiaddrs.length) addrs.forEach((addr, index) => { expect(addr.isCertified).to.eql(false) expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) }) // Create peer record const peerRecord = new PeerRecord({ peerId, multiaddrs }) const envelope = await Envelope.seal(peerRecord, peerId) peerStore.once('change:multiaddrs', ({ peerId, multiaddrs }) => { expect(peerId).to.exist() expect(multiaddrs).to.eql(multiaddrs) defer.resolve() }) // consume peer record const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(true) // Wait event await defer.promise // Validate data exists and certified addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(multiaddrs.length) addrs.forEach((addr, index) => { expect(addr.isCertified).to.eql(true) expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) }) }) it('emits change:multiaddrs event with previous partial data in AddressBook (not certified)', async () => { const defer = pDefer() const multiaddrs = [addr1, addr2] // Set addressBook data await ab.set(peerId, [addr1]) // Validate data exists, but not certified let addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(1) expect(addrs[0].isCertified).to.eql(false) expect(addrs[0].multiaddr.equals(addr1)).to.eql(true) // Create peer record const peerRecord = new PeerRecord({ peerId, multiaddrs }) const envelope = await Envelope.seal(peerRecord, peerId) peerStore.once('change:multiaddrs', ({ peerId, multiaddrs }) => { expect(peerId).to.exist() expect(multiaddrs).to.eql(multiaddrs) defer.resolve() }) // consume peer record const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(true) // Wait event await defer.promise // Validate data exists and certified addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(multiaddrs.length) addrs.forEach((addr, index) => { expect(addr.isCertified).to.eql(true) expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) }) }) it('with previous different data in AddressBook (not certified)', async () => { const defer = pDefer() const multiaddrsUncertified = [addr3] const multiaddrsCertified = [addr1, addr2] // Set addressBook data await ab.set(peerId, multiaddrsUncertified) // Validate data exists, but not certified let addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(multiaddrsUncertified.length) addrs.forEach((addr, index) => { expect(addr.isCertified).to.eql(false) expect(multiaddrsUncertified[index].equals(addr.multiaddr)).to.eql(true) }) // Create peer record const peerRecord = new PeerRecord({ peerId, multiaddrs: multiaddrsCertified }) const envelope = await Envelope.seal(peerRecord, peerId) peerStore.once('change:multiaddrs', ({ peerId, multiaddrs }) => { expect(peerId).to.exist() expect(multiaddrs).to.eql(multiaddrs) defer.resolve() }) // consume peer record const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(true) // Wait event await defer.promise // Validate data exists and certified addrs = await ab.get(peerId) expect(addrs).to.exist() expect(addrs).to.have.lengthOf(multiaddrsCertified.length) addrs.forEach((addr, index) => { expect(addr.isCertified).to.eql(true) expect(multiaddrsCertified[index].equals(addr.multiaddr)).to.eql(true) }) }) }) describe('fails to consume invalid peer records', () => { beforeEach(() => { peerStore = new PeerStore({ peerId, datastore: new MemoryDatastore(), addressFilter: connectionGater.filterMultiaddrForPeer }) ab = peerStore.addressBook }) it('invalid peer record', async () => { const invalidEnvelope = { payload: Buffer.from('invalid-peerRecord') } const consumed = await ab.consumePeerRecord(invalidEnvelope) expect(consumed).to.eql(false) }) it('peer that created the envelope is not the same as the peer record', async () => { const multiaddrs = [addr1, addr2] // Create peer record const peerId2 = await PeerId.create() const peerRecord = new PeerRecord({ peerId: peerId2, multiaddrs }) const envelope = await Envelope.seal(peerRecord, peerId) const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(false) }) it('does not store an outdated record', async () => { const multiaddrs = [addr1, addr2] const peerRecord1 = new PeerRecord({ peerId, multiaddrs, seqNumber: Date.now() }) const peerRecord2 = new PeerRecord({ peerId, multiaddrs, seqNumber: Date.now() - 1 }) const envelope1 = await Envelope.seal(peerRecord1, peerId) const envelope2 = await Envelope.seal(peerRecord2, peerId) // Consume envelope1 (bigger seqNumber) let consumed = await ab.consumePeerRecord(envelope1) expect(consumed).to.eql(true) consumed = await ab.consumePeerRecord(envelope2) expect(consumed).to.eql(false) }) it('empty multiaddrs', async () => { const peerRecord = new PeerRecord({ peerId, multiaddrs: [] }) const envelope = await Envelope.seal(peerRecord, peerId) const consumed = await ab.consumePeerRecord(envelope) expect(consumed).to.eql(false) }) }) }) })