js-libp2p/test/identify/index.spec.ts
Alex Potsides a1220d22f5
fix: time out slow reads (#1227)
There are a few places in the codebase where we send/receive data from the network without timeouts/abort controllers which means the user has to wait for the underlying socket to timeout which can take a long time depending on the platform, if at all.

This change ensures we can time out while running identify (both flavours), ping and fetch and adds tests to ensure there are no regressions.
2022-05-25 18:15:21 +01:00

274 lines
9.3 KiB
TypeScript

/* eslint-env mocha */
import { expect } from 'aegir/chai'
import sinon from 'sinon'
import { Multiaddr } from '@multiformats/multiaddr'
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 { PersistentPeerStore } from '@libp2p/peer-store'
import { DefaultAddressManager } from '../../src/address-manager/index.js'
import { MemoryDatastore } from 'datastore-core/memory'
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 delay from 'delay'
import { start, stop } from '@libp2p/interfaces/startable'
import { TimeoutController } from 'timeout-abort-controller'
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) {
const peerId = await createFromJSON(Peers[index])
const components = new Components({
peerId,
datastore: new MemoryDatastore(),
registrar: mockRegistrar(),
upgrader: mockUpgrader(),
connectionGater: mockConnectionGater(),
peerStore: new PersistentPeerStore(),
connectionManager: new DefaultConnectionManager({
minConnections: 50,
maxConnections: 1000,
autoDialInterval: 1000
})
})
components.setAddressManager(new DefaultAddressManager(components, {
announce: listenMaddrs.map(ma => ma.toString())
}))
const transportManager = new DefaultTransportManager(components)
components.setTransportManager(transportManager)
await components.getPeerStore().protoBook.set(peerId, protocols)
return components
}
describe('identify', () => {
let localComponents: Components
let remoteComponents: Components
let remotePeerRecordUpdater: PeerRecordUpdater
beforeEach(async () => {
localComponents = await createComponents(0)
remoteComponents = await createComponents(1)
remotePeerRecordUpdater = new PeerRecordUpdater(remoteComponents)
await Promise.all([
start(localComponents),
start(remoteComponents)
])
})
afterEach(async () => {
sinon.restore()
await Promise.all([
stop(localComponents),
stop(remoteComponents)
])
})
it('should be able to identify another peer', async () => {
const localIdentify = new IdentifyService(localComponents, defaultInit)
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
await start(localIdentify)
await start(remoteIdentify)
const [localToRemote] = connectionPair(localComponents, remoteComponents)
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 start(localIdentify)
const remoteIdentify = new IdentifyService(remoteComponents, {
protocolPrefix: 'ipfs',
host: {
agentVersion: agentVersion
}
})
await start(remoteIdentify)
const [localToRemote] = connectionPair(localComponents, remoteComponents)
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 start(localIdentify)
await start(remoteIdentify)
const [localToRemote] = connectionPair(localComponents, remoteComponents)
// 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 start(localIdentify)
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 stop(localIdentify)
})
it('should time out during identify', async () => {
const localIdentify = new IdentifyService(localComponents, defaultInit)
const remoteIdentify = new IdentifyService(remoteComponents, defaultInit)
await start(localIdentify)
await start(remoteIdentify)
const [localToRemote] = connectionPair(localComponents, remoteComponents)
// replace existing handler with a really slow one
await remoteComponents.getRegistrar().unhandle(MULTICODEC_IDENTIFY)
await remoteComponents.getRegistrar().handle(MULTICODEC_IDENTIFY, ({ stream }) => {
void pipe(
stream,
async function * (source) {
// we receive no data in the identify protocol, we just send our data
await drain(source)
// longer than the timeout
await delay(1000)
yield new Uint8Array()
},
stream
)
})
const newStreamSpy = sinon.spy(localToRemote, 'newStream')
// 10 ms timeout
const timeoutController = new TimeoutController(10)
// Run identify
await expect(localIdentify.identify(localToRemote, {
signal: timeoutController.signal
}))
.to.eventually.be.rejected.with.property('code', 'ABORT_ERR')
// should have closed stream
expect(newStreamSpy).to.have.property('callCount', 1)
const { stream } = await newStreamSpy.getCall(0).returnValue
expect(stream).to.have.nested.property('timeline.close')
})
})