js-libp2p/test/identify/index.spec.ts
Alex Potsides c64a586a20
chore: update aegir to the latest version (#1186)
Removes boilerplate config that is no longer necessary
2022-04-09 09:26:25 +01:00

620 lines
22 KiB
TypeScript

/* eslint-env mocha */
import { expect } from 'aegir/chai'
import sinon from 'sinon'
import { Multiaddr } from '@multiformats/multiaddr'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { codes } from '../../src/errors.js'
import { IdentifyService, Message } from '../../src/identify/index.js'
import Peers from '../fixtures/peers.js'
import { createLibp2pNode } from '../../src/libp2p.js'
import { PersistentPeerStore } from '@libp2p/peer-store'
import { createBaseOptions } from '../utils/base-options.browser.js'
import { DefaultAddressManager } from '../../src/address-manager/index.js'
import { MemoryDatastore } from 'datastore-core/memory'
import { MULTIADDRS_WEBSOCKETS } from '../fixtures/browser.js'
import * as lp from 'it-length-prefixed'
import drain from 'it-drain'
import { pipe } from 'it-pipe'
import { mockConnectionGater, mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks'
import { createFromJSON } from '@libp2p/peer-id-factory'
import { Components } from '@libp2p/interfaces/components'
import { PeerRecordUpdater } from '../../src/peer-record-updater.js'
import {
MULTICODEC_IDENTIFY,
MULTICODEC_IDENTIFY_PUSH
} from '../../src/identify/consts.js'
import { DefaultConnectionManager } from '../../src/connection-manager/index.js'
import { DefaultTransportManager } from '../../src/transport-manager.js'
import { CustomEvent, Startable } from '@libp2p/interfaces'
import delay from 'delay'
import pWaitFor from 'p-wait-for'
import { peerIdFromString } from '@libp2p/peer-id'
import type { PeerId } from '@libp2p/interfaces/peer-id'
import type { Libp2pNode } from '../../src/libp2p.js'
import { pEvent } from 'p-event'
const listenMaddrs = [new Multiaddr('/ip4/127.0.0.1/tcp/15002/ws')]
const defaultInit = {
protocolPrefix: 'ipfs',
host: {
agentVersion: 'v1.0.0'
}
}
const protocols = [MULTICODEC_IDENTIFY, MULTICODEC_IDENTIFY_PUSH]
async function createComponents (index: number, services: Startable[]) {
const peerId = await createFromJSON(Peers[index])
const components = new Components({
peerId,
datastore: new MemoryDatastore(),
registrar: mockRegistrar(),
upgrader: mockUpgrader(),
connectionGater: mockConnectionGater()
})
const peerStore = new PersistentPeerStore(components, {
addressFilter: components.getConnectionGater().filterMultiaddrForPeer
})
components.setPeerStore(peerStore)
components.setAddressManager(new DefaultAddressManager(components, {
announce: listenMaddrs.map(ma => ma.toString())
}))
const connectionManager = new DefaultConnectionManager(components)
services.push(connectionManager)
components.setConnectionManager(connectionManager)
const transportManager = new DefaultTransportManager(components)
services.push(transportManager)
components.setTransportManager(transportManager)
await peerStore.protoBook.set(peerId, protocols)
return components
}
describe('Identify', () => {
let localComponents: Components
let remoteComponents: Components
let localPeerRecordUpdater: PeerRecordUpdater
let remotePeerRecordUpdater: PeerRecordUpdater
let services: Startable[]
beforeEach(async () => {
services = []
localComponents = await createComponents(0, services)
remoteComponents = await createComponents(1, services)
localPeerRecordUpdater = new PeerRecordUpdater(localComponents)
remotePeerRecordUpdater = new PeerRecordUpdater(remoteComponents)
await Promise.all(
services.map(s => s.start())
)
})
afterEach(async () => {
sinon.restore()
await Promise.all(
services.map(s => s.stop())
)
})
it('should be able to identify another peer', async () => {
const localIdentify = new IdentifyService(localComponents, defaultInit)
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
await localIdentify.start()
await remoteIdentify.start()
const [localToRemote] = connectionPair({
peerId: localComponents.getPeerId(),
registrar: localComponents.getRegistrar()
}, {
peerId: remoteComponents.getPeerId(),
registrar: remoteComponents.getRegistrar()
})
const localAddressBookConsumePeerRecordSpy = sinon.spy(localComponents.getPeerStore().addressBook, 'consumePeerRecord')
const localProtoBookSetSpy = sinon.spy(localComponents.getPeerStore().protoBook, 'set')
// Make sure the remote peer has a peer record to share during identify
await remotePeerRecordUpdater.update()
// Run identify
await localIdentify.identify(localToRemote)
expect(localAddressBookConsumePeerRecordSpy.callCount).to.equal(1)
expect(localProtoBookSetSpy.callCount).to.equal(1)
// Validate the remote peer gets updated in the peer store
const addresses = await localComponents.getPeerStore().addressBook.get(remoteComponents.getPeerId())
expect(addresses).to.exist()
expect(addresses).have.lengthOf(listenMaddrs.length)
expect(addresses.map((a) => a.multiaddr)[0].equals(listenMaddrs[0]))
expect(addresses.map((a) => a.isCertified)[0]).to.be.true()
})
// LEGACY
it('should be able to identify another peer with no certified peer records support', async () => {
const agentVersion = 'js-libp2p/5.0.0'
const localIdentify = new IdentifyService(localComponents, {
protocolPrefix: 'ipfs',
host: {
agentVersion: agentVersion
}
})
await localIdentify.start()
const remoteIdentify = new IdentifyService(remoteComponents, {
protocolPrefix: 'ipfs',
host: {
agentVersion: agentVersion
}
})
await remoteIdentify.start()
const [localToRemote] = connectionPair({
peerId: localComponents.getPeerId(),
registrar: localComponents.getRegistrar()
}, {
peerId: remoteComponents.getPeerId(),
registrar: remoteComponents.getRegistrar()
})
sinon.stub(localComponents.getPeerStore().addressBook, 'consumePeerRecord').throws()
const localProtoBookSetSpy = sinon.spy(localComponents.getPeerStore().protoBook, 'set')
// Run identify
await localIdentify.identify(localToRemote)
expect(localProtoBookSetSpy.callCount).to.equal(1)
// Validate the remote peer gets updated in the peer store
const addresses = await localComponents.getPeerStore().addressBook.get(remoteComponents.getPeerId())
expect(addresses).to.exist()
expect(addresses).have.lengthOf(listenMaddrs.length)
expect(addresses.map((a) => a.multiaddr)[0].equals(listenMaddrs[0]))
expect(addresses.map((a) => a.isCertified)[0]).to.be.false()
})
it('should throw if identified peer is the wrong peer', async () => {
const localIdentify = new IdentifyService(localComponents, defaultInit)
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
await localIdentify.start()
await remoteIdentify.start()
const [localToRemote] = connectionPair({
peerId: localComponents.getPeerId(),
registrar: localComponents.getRegistrar()
}, {
peerId: remoteComponents.getPeerId(),
registrar: remoteComponents.getRegistrar()
})
// send an invalid message
await remoteComponents.getRegistrar().unhandle(MULTICODEC_IDENTIFY)
await remoteComponents.getRegistrar().handle(MULTICODEC_IDENTIFY, (data) => {
void Promise.resolve().then(async () => {
const { connection, stream } = data
const signedPeerRecord = await remoteComponents.getPeerStore().addressBook.getRawEnvelope(remoteComponents.getPeerId())
const message = Message.Identify.encode({
protocolVersion: '123',
agentVersion: '123',
// send bad public key
publicKey: localComponents.getPeerId().publicKey ?? new Uint8Array(0),
listenAddrs: [],
signedPeerRecord,
observedAddr: connection.remoteAddr.bytes,
protocols: []
})
await pipe(
[message],
lp.encode(),
stream,
drain
)
})
})
// Run identify
await expect(localIdentify.identify(localToRemote))
.to.eventually.be.rejected()
.and.to.have.property('code', codes.ERR_INVALID_PEER)
})
it('should store own host data and protocol version into metadataBook on start', async () => {
const agentVersion = 'js-project/1.0.0'
const localIdentify = new IdentifyService(localComponents, {
protocolPrefix: 'ipfs',
host: {
agentVersion
}
})
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'AgentVersion'))
.to.eventually.be.undefined()
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'ProtocolVersion'))
.to.eventually.be.undefined()
await localIdentify.start()
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'AgentVersion'))
.to.eventually.deep.equal(uint8ArrayFromString(agentVersion))
await expect(localComponents.getPeerStore().metadataBook.getValue(localComponents.getPeerId(), 'ProtocolVersion'))
.to.eventually.be.ok()
await localIdentify.stop()
})
describe('push', () => {
it('should be able to push identify updates to another peer', async () => {
const localIdentify = new IdentifyService(localComponents, defaultInit)
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
await localIdentify.start()
await remoteIdentify.start()
const [localToRemote, remoteToLocal] = connectionPair({
peerId: localComponents.getPeerId(),
registrar: localComponents.getRegistrar()
}, {
peerId: remoteComponents.getPeerId(),
registrar: remoteComponents.getRegistrar()
})
// ensure connections are registered by connection manager
localComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', {
detail: localToRemote
}))
remoteComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', {
detail: remoteToLocal
}))
// identify both ways
await localIdentify.identify(localToRemote)
await remoteIdentify.identify(remoteToLocal)
const updatedProtocol = '/special-new-protocol/1.0.0'
const updatedAddress = new Multiaddr('/ip4/127.0.0.1/tcp/48322')
// should have protocols but not our new one
const identifiedProtocols = await remoteComponents.getPeerStore().protoBook.get(localComponents.getPeerId())
expect(identifiedProtocols).to.not.be.empty()
expect(identifiedProtocols).to.not.include(updatedProtocol)
// should have addresses but not our new one
const identifiedAddresses = await remoteComponents.getPeerStore().addressBook.get(localComponents.getPeerId())
expect(identifiedAddresses).to.not.be.empty()
expect(identifiedAddresses.map(a => a.multiaddr.toString())).to.not.include(updatedAddress.toString())
// update local data - change event will trigger push
await localComponents.getPeerStore().protoBook.add(localComponents.getPeerId(), [updatedProtocol])
await localComponents.getPeerStore().addressBook.add(localComponents.getPeerId(), [updatedAddress])
// needed to update the peer record and send our supported addresses
const addressManager = localComponents.getAddressManager()
addressManager.getAddresses = () => {
return [updatedAddress]
}
// ensure sequence number of peer record we are about to create is different
await delay(1000)
// make sure we have a peer record to send
await localPeerRecordUpdater.update()
// wait for the remote peer store to notice the changes
const eventPromise = pEvent(remoteComponents.getPeerStore(), 'change:multiaddrs')
// push updated peer record to connections
await localIdentify.pushToPeerStore()
await eventPromise
// should have new protocol
const updatedProtocols = await remoteComponents.getPeerStore().protoBook.get(localComponents.getPeerId())
expect(updatedProtocols).to.not.be.empty()
expect(updatedProtocols).to.include(updatedProtocol)
// should have new address
const updatedAddresses = await remoteComponents.getPeerStore().addressBook.get(localComponents.getPeerId())
expect(updatedAddresses.map(a => {
return {
multiaddr: a.multiaddr.toString(),
isCertified: a.isCertified
}
})).to.deep.equal([{
multiaddr: updatedAddress.toString(),
isCertified: true
}])
await localIdentify.stop()
await remoteIdentify.stop()
})
// LEGACY
it('should be able to push identify updates to another peer with no certified peer records support', async () => {
const localIdentify = new IdentifyService(localComponents, defaultInit)
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
await localIdentify.start()
await remoteIdentify.start()
const [localToRemote, remoteToLocal] = connectionPair({
peerId: localComponents.getPeerId(),
registrar: localComponents.getRegistrar()
}, {
peerId: remoteComponents.getPeerId(),
registrar: remoteComponents.getRegistrar()
})
// ensure connections are registered by connection manager
localComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', {
detail: localToRemote
}))
remoteComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', {
detail: remoteToLocal
}))
// identify both ways
await localIdentify.identify(localToRemote)
await remoteIdentify.identify(remoteToLocal)
const updatedProtocol = '/special-new-protocol/1.0.0'
const updatedAddress = new Multiaddr('/ip4/127.0.0.1/tcp/48322')
// should have protocols but not our new one
const identifiedProtocols = await remoteComponents.getPeerStore().protoBook.get(localComponents.getPeerId())
expect(identifiedProtocols).to.not.be.empty()
expect(identifiedProtocols).to.not.include(updatedProtocol)
// should have addresses but not our new one
const identifiedAddresses = await remoteComponents.getPeerStore().addressBook.get(localComponents.getPeerId())
expect(identifiedAddresses).to.not.be.empty()
expect(identifiedAddresses.map(a => a.multiaddr.toString())).to.not.include(updatedAddress.toString())
// update local data - change event will trigger push
await localComponents.getPeerStore().protoBook.add(localComponents.getPeerId(), [updatedProtocol])
await localComponents.getPeerStore().addressBook.add(localComponents.getPeerId(), [updatedAddress])
// needed to send our supported addresses
const addressManager = localComponents.getAddressManager()
addressManager.getAddresses = () => {
return [updatedAddress]
}
// wait until remote peer store notices protocol list update
const waitForUpdate = pEvent(remoteComponents.getPeerStore(), 'change:protocols')
await localIdentify.pushToPeerStore()
await waitForUpdate
// should have new protocol
const updatedProtocols = await remoteComponents.getPeerStore().protoBook.get(localComponents.getPeerId())
expect(updatedProtocols).to.not.be.empty()
expect(updatedProtocols).to.include(updatedProtocol)
// should have new address
const updatedAddresses = await remoteComponents.getPeerStore().addressBook.get(localComponents.getPeerId())
expect(updatedAddresses.map(a => {
return {
multiaddr: a.multiaddr.toString(),
isCertified: a.isCertified
}
})).to.deep.equal([{
multiaddr: updatedAddress.toString(),
isCertified: false
}])
await localIdentify.stop()
await remoteIdentify.stop()
})
})
describe('libp2p.dialer.identifyService', () => {
let peerId: PeerId
let libp2p: Libp2pNode
let remoteLibp2p: Libp2pNode
const remoteAddr = MULTIADDRS_WEBSOCKETS[0]
before(async () => {
peerId = await createFromJSON(Peers[0])
})
afterEach(async () => {
sinon.restore()
if (libp2p != null) {
await libp2p.stop()
}
})
after(async () => {
if (remoteLibp2p != null) {
await remoteLibp2p.stop()
}
})
it('should run identify automatically after connecting', async () => {
libp2p = await createLibp2pNode(createBaseOptions({
peerId
}))
await libp2p.start()
if (libp2p.identifyService == null) {
throw new Error('Identity service was not configured')
}
const identityServiceIdentifySpy = sinon.spy(libp2p.identifyService, 'identify')
const peerStoreSpyConsumeRecord = sinon.spy(libp2p.peerStore.addressBook, 'consumePeerRecord')
const peerStoreSpyAdd = sinon.spy(libp2p.peerStore.addressBook, 'add')
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
// Wait for peer store to be updated
// Dialer._createDialTarget (add), Identify (consume)
await pWaitFor(() => peerStoreSpyConsumeRecord.callCount === 1 && peerStoreSpyAdd.callCount === 1)
expect(identityServiceIdentifySpy.callCount).to.equal(1)
// The connection should have no open streams
await pWaitFor(() => connection.streams.length === 0)
await connection.close()
})
it('should store remote agent and protocol versions in metadataBook after connecting', async () => {
libp2p = await createLibp2pNode(createBaseOptions({
peerId
}))
await libp2p.start()
if (libp2p.identifyService == null) {
throw new Error('Identity service was not configured')
}
const identityServiceIdentifySpy = sinon.spy(libp2p.identifyService, 'identify')
const peerStoreSpyConsumeRecord = sinon.spy(libp2p.peerStore.addressBook, 'consumePeerRecord')
const peerStoreSpyAdd = sinon.spy(libp2p.peerStore.addressBook, 'add')
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
// Wait for peer store to be updated
// Dialer._createDialTarget (add), Identify (consume)
await pWaitFor(() => peerStoreSpyConsumeRecord.callCount === 1 && peerStoreSpyAdd.callCount === 1)
expect(identityServiceIdentifySpy.callCount).to.equal(1)
// The connection should have no open streams
await pWaitFor(() => connection.streams.length === 0)
await connection.close()
const remotePeer = peerIdFromString(remoteAddr.getPeerId() ?? '')
const storedAgentVersion = await libp2p.peerStore.metadataBook.getValue(remotePeer, 'AgentVersion')
const storedProtocolVersion = await libp2p.peerStore.metadataBook.getValue(remotePeer, 'ProtocolVersion')
expect(storedAgentVersion).to.exist()
expect(storedProtocolVersion).to.exist()
})
it('should push protocol updates to an already connected peer', async () => {
libp2p = await createLibp2pNode(createBaseOptions({
peerId
}))
await libp2p.start()
if (libp2p.identifyService == null) {
throw new Error('Identity service was not configured')
}
const identityServiceIdentifySpy = sinon.spy(libp2p.identifyService, 'identify')
const identityServicePushSpy = sinon.spy(libp2p.identifyService, 'push')
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
// Wait for identify to finish
await identityServiceIdentifySpy.firstCall.returnValue
sinon.stub(libp2p, 'isStarted').returns(true)
await libp2p.handle('/echo/2.0.0', () => {})
await libp2p.unhandle('/echo/2.0.0')
// the protocol change event listener in the identity service is async
await pWaitFor(() => identityServicePushSpy.callCount === 2)
// Verify the remote peer is notified of both changes
expect(identityServicePushSpy.callCount).to.equal(2)
for (const call of identityServicePushSpy.getCalls()) {
const [connections] = call.args
expect(connections.length).to.equal(1)
expect(connections[0].remotePeer.toString()).to.equal(remoteAddr.getPeerId())
await call.returnValue
}
// Verify the streams close
await pWaitFor(() => connection.streams.length === 0)
})
it('should store host data and protocol version into metadataBook', async () => {
const agentVersion = 'js-project/1.0.0'
libp2p = await createLibp2pNode(createBaseOptions({
peerId,
host: {
agentVersion
}
}))
await libp2p.start()
if (libp2p.identifyService == null) {
throw new Error('Identity service was not configured')
}
const storedAgentVersion = await libp2p.peerStore.metadataBook.getValue(peerId, 'AgentVersion')
const storedProtocolVersion = await libp2p.peerStore.metadataBook.getValue(peerId, 'ProtocolVersion')
expect(agentVersion).to.equal(uint8ArrayToString(storedAgentVersion ?? new Uint8Array()))
expect(storedProtocolVersion).to.exist()
})
it('should push multiaddr updates to an already connected peer', async () => {
libp2p = await createLibp2pNode(createBaseOptions({
peerId
}))
await libp2p.start()
if (libp2p.identifyService == null) {
throw new Error('Identity service was not configured')
}
const identityServiceIdentifySpy = sinon.spy(libp2p.identifyService, 'identify')
const identityServicePushSpy = sinon.spy(libp2p.identifyService, 'push')
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
// Wait for identify to finish
await identityServiceIdentifySpy.firstCall.returnValue
sinon.stub(libp2p, 'isStarted').returns(true)
await libp2p.peerStore.addressBook.add(libp2p.peerId, [new Multiaddr('/ip4/180.0.0.1/tcp/15001/ws')])
// the protocol change event listener in the identity service is async
await pWaitFor(() => identityServicePushSpy.callCount === 1)
// Verify the remote peer is notified of change
expect(identityServicePushSpy.callCount).to.equal(1)
for (const call of identityServicePushSpy.getCalls()) {
const [connections] = call.args
expect(connections.length).to.equal(1)
expect(connections[0].remotePeer.toString()).to.equal(remoteAddr.getPeerId())
await call.returnValue
}
// Verify the streams close
await pWaitFor(() => connection.streams.length === 0)
})
})
})