diff --git a/packages/bitswap/src/network.ts b/packages/bitswap/src/network.ts index 95ab94cb0..30d11a9b8 100644 --- a/packages/bitswap/src/network.ts +++ b/packages/bitswap/src/network.ts @@ -1,4 +1,4 @@ -import { InvalidParametersError, NotStartedError, TimeoutError, TypedEventEmitter, UnsupportedProtocolError, setMaxListeners } from '@libp2p/interface' +import { InvalidParametersError, NotStartedError, TimeoutError, TypedEventEmitter, UnsupportedProtocolError, isPeerId, setMaxListeners } from '@libp2p/interface' import { PeerQueue } from '@libp2p/utils' import drain from 'it-drain' import * as lp from 'it-length-prefixed' @@ -406,6 +406,21 @@ export class Network extends TypedEventEmitter { options?.onProgress?.(new CustomProgressEvent('bitswap:dial', peer)) + // Fast path: peer:identify is single-shot per peer, so if the peer was + // already identified (e.g. dialed by another subsystem) the raceEvent + // below would wait for an event that has already fired. When the peerStore + // already lists BITSWAP_120 for this peer we can skip the race entirely. + if (isPeerId(peer)) { + try { + const peerData = await this.libp2p.peerStore.get(peer) + if (peerData.protocols.includes(BITSWAP_120)) { + return await this.libp2p.dial(peer, options) + } + } catch { + // peer not in peerStore yet — fall through to the identify race + } + } + // dial and wait for identify - this is to avoid opening a protocol stream // that we are not going to use but depends on the remote node running the // identify protocol diff --git a/packages/bitswap/test/network.spec.ts b/packages/bitswap/test/network.spec.ts index a66bcd9f5..e0a75fc5c 100644 --- a/packages/bitswap/test/network.spec.ts +++ b/packages/bitswap/test/network.spec.ts @@ -66,6 +66,73 @@ describe('network', () => { .with.property('name', 'NotStartedError') }) + it('should not wait for peer:identify when the peer is already known to speak bitswap', async () => { + // peer:identify is single-shot per peer. If the peer was already + // identified by another subsystem (e.g. pubsub mesh warmup) before + // bitswap calls connectTo, awaiting peer:identify hangs forever. When + // the peerStore already lists BITSWAP_120 for the peer the race is + // unnecessary and connectTo must resolve from dial() alone. + const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const connection = stubInterface() + + components.libp2p.peerStore = stubInterface({ + get: Sinon.stub().withArgs(peerId).resolves({ + id: peerId, + addresses: [], + protocols: [BITSWAP_120], + metadata: new Map(), + tags: new Map() + }) + }) as any + + components.libp2p.dial.withArgs(peerId).resolves(connection) + + const result = await network.connectTo(peerId) + expect(result).to.equal(connection) + expect(components.libp2p.dial.calledWith(peerId)).to.be.true() + // raceEvent listens via addEventListener; the fast path skips it entirely + const identifyListens = components.libp2p.addEventListener.getCalls() + .filter(call => call.args[0] === 'peer:identify') + expect(identifyListens, 'should not subscribe to peer:identify on the fast path').to.have.lengthOf(0) + }) + + it('should fall through to the identify race when the peer is not yet in peerStore', async () => { + const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const connection = stubInterface() + const peerStoreError = new Error('NotFoundError') + peerStoreError.name = 'NotFoundError' + + components.libp2p.peerStore = stubInterface({ + get: Sinon.stub().rejects(peerStoreError) + }) as any + + components.libp2p.dial.callsFake(async () => { + // simulate identify firing after dial + setTimeout(() => { + const call = components.libp2p.addEventListener.getCalls() + .find(c => c.args[0] === 'peer:identify') + const callback = call?.args[1] + if (typeof callback === 'function') { + callback(new CustomEvent('peer:identify', { + detail: { + peerId, + protocols: [BITSWAP_120], + listenAddrs: [], + connection + } + })) + } + }, 10) + return connection + }) + + await network.connectTo(peerId) + + const identifyListens = components.libp2p.addEventListener.getCalls() + .filter(call => call.args[0] === 'peer:identify') + expect(identifyListens, 'should subscribe to peer:identify on the slow path').to.have.lengthOf.at.least(1) + }) + it('should register protocol handlers', () => { expect(components.libp2p.handle.called).to.be.true() expect(components.libp2p.register.calledWith(BITSWAP_120)).to.be.true()