diff --git a/packages/libp2p/src/connection-manager/dial-queue.ts b/packages/libp2p/src/connection-manager/dial-queue.ts index dc40c52dbb..b72d6720fe 100644 --- a/packages/libp2p/src/connection-manager/dial-queue.ts +++ b/packages/libp2p/src/connection-manager/dial-queue.ts @@ -226,10 +226,12 @@ export class DialQueue { // if we have no multiaddrs, only a peer id, set a flag so we will look the // peer up in the peer routing to obtain multiaddrs - let forcePeerLookup = options.multiaddrs.size === 0 + const peerIdOnlyDial = options.multiaddrs.size === 0 + let forcePeerLookup = peerIdOnlyDial let dialed = 0 let dialIteration = 0 + let retriedNoValidAddresses = false const errors: Error[] = [] this.log('starting dial to %p', peerId) @@ -256,10 +258,23 @@ export class DialQueue { // load addresses from address book, resolve and dnsaddrs, filter // undialables, add peer IDs, etc - const calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, { - ...options, - signal - }) + let calculatedAddrs: Address[] + + try { + calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, { + ...options, + signal + }) + } catch (err: any) { + if (err.name === NoValidAddressesError.name && peerId != null && peerIdOnlyDial && !retriedNoValidAddresses) { + this.log('no valid addresses for %p, retrying once to pick up newly discovered peer store addresses', peerId) + retriedNoValidAddresses = true + forcePeerLookup = true + continue + } + + throw err + } for (const addr of calculatedAddrs) { // skip any addresses we have previously failed to dial diff --git a/packages/libp2p/test/connection-manager/index.spec.ts b/packages/libp2p/test/connection-manager/index.spec.ts index 36b66c365e..6885385729 100644 --- a/packages/libp2p/test/connection-manager/index.spec.ts +++ b/packages/libp2p/test/connection-manager/index.spec.ts @@ -1,5 +1,5 @@ import { generateKeyPair } from '@libp2p/crypto/keys' -import { InvalidParametersError, KEEP_ALIVE, start, stop } from '@libp2p/interface' +import { InvalidParametersError, KEEP_ALIVE, NotFoundError, start, stop } from '@libp2p/interface' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' @@ -11,7 +11,7 @@ import { createLibp2p } from '../../src/index.js' import { getComponent } from '../fixtures/get-component.js' import { createDefaultConnectionManagerComponents } from './utils.js' import type { StubbedDefaultConnectionManagerComponents } from './utils.js' -import type { Libp2p, Connection, MultiaddrConnection } from '@libp2p/interface' +import type { Libp2p, Connection, MultiaddrConnection, Transport } from '@libp2p/interface' const defaultOptions = { maxConnections: 10, @@ -419,6 +419,70 @@ describe('Connection Manager', () => { expect(conn).to.equal(newConnection) }) + it('should retry peer-id openConnection when peer store gains an address during dial', async () => { + connectionManager = new DefaultConnectionManager(components, defaultOptions) + await connectionManager.start() + + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const discoveredAddr = multiaddr('/ip4/123.123.123.123/tcp/4001') + const discoveredAddrWithPeer = discoveredAddr.encapsulate(`/p2p/${remotePeer}`) + + let resolvePeerRouting: (() => void) | undefined + const peerRoutingBlocked = new Promise((resolve) => { + resolvePeerRouting = resolve + }) + + const peerStoreReadStarted = pWaitFor(async () => components.peerStore.get.calledOnce) + let hasAddress = false + + components.peerStore.get.callsFake(async () => { + if (!hasAddress) { + throw new NotFoundError('Not found') + } + + return { + id: remotePeer, + addresses: [{ + multiaddr: discoveredAddr, + isCertified: false + }], + protocols: [], + metadata: new Map(), + tags: new Map() + } + }) + + components.peerRouting.findPeer.callsFake(async () => { + await peerRoutingBlocked + + return { + id: remotePeer, + multiaddrs: [] + } + }) + + components.transportManager.dialTransportForMultiaddr.returns(stubInterface()) + const connection = stubInterface({ + remotePeer, + remoteAddr: discoveredAddrWithPeer, + status: 'open' + }) + components.transportManager.dial.callsFake(async (ma) => { + expect(ma.equals(discoveredAddrWithPeer)).to.equal(true) + return connection + }) + + const dialPromise = connectionManager.openConnection(remotePeer) + await peerStoreReadStarted + + hasAddress = true + resolvePeerRouting?.() + + await expect(dialPromise).to.eventually.equal(connection) + expect(components.peerStore.get.callCount).to.equal(2) + expect(components.transportManager.dial.calledOnce).to.equal(true) + }) + it('should throw when setMaxConnections is less than 1', async () => { connectionManager = new DefaultConnectionManager(components, { ...defaultOptions,