Skip to content

Commit 2c56203

Browse files
authored
feat: add isDialable method to libp2p (#2479)
Dialing peers is expensive, as are peer routing queries. Dials can be rejected due to configuration (no transport, connection gating, etc) - if a user is in the middle of a routing query they may wish to test dialability and continue the query instead of assuming they've found a dialable peer. Adds an `isDialable` method to libp2p that given a multiaddr or multaddrs, allows testing them to ensure libp2p won't immediately reject the dial attempt due to how the node has been configured.
1 parent c5003d4 commit 2c56203

File tree

8 files changed

+125
-10
lines changed

8 files changed

+125
-10
lines changed

packages/interface-compliance-tests/src/mocks/connection-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ class MockConnectionManager implements ConnectionManager, Startable {
199199
getDialQueue (): PendingDial[] {
200200
return []
201201
}
202+
203+
async isDialable (): Promise<boolean> {
204+
return true
205+
}
202206
}
203207

204208
export function mockConnectionManager (components: MockConnectionManagerComponents): ConnectionManager {

packages/interface-internal/src/connection-manager/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AbortOptions, PendingDial, Connection, MultiaddrConnection, PeerId } from '@libp2p/interface'
1+
import type { AbortOptions, PendingDial, Connection, MultiaddrConnection, PeerId, IsDialableOptions } from '@libp2p/interface'
22
import type { PeerMap } from '@libp2p/peer-collections'
33
import type { Multiaddr } from '@multiformats/multiaddr'
44

@@ -80,4 +80,15 @@ export interface ConnectionManager {
8080
* ```
8181
*/
8282
getDialQueue(): PendingDial[]
83+
84+
/**
85+
* Given the current node configuration, returns a promise of `true` or
86+
* `false` if the node would attempt to dial the passed multiaddr.
87+
*
88+
* This means a relevant transport is configured, and the connection gater
89+
* would not block the dial attempt.
90+
*
91+
* This may involve resolving DNS addresses so you should pass an AbortSignal.
92+
*/
93+
isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise<boolean>
8394
}

packages/interface/src/index.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ export interface PendingDial {
324324

325325
export type Libp2pStatus = 'starting' | 'started' | 'stopping' | 'stopped'
326326

327+
export interface IsDialableOptions extends AbortOptions {
328+
/**
329+
* If the dial attempt would open a protocol, and the multiaddr being dialed
330+
* is a circuit relay address, passing true here would cause the test to fail
331+
* because that protocol would not be allowed to run over a data/time limited
332+
* connection.
333+
*/
334+
runOnTransientConnection?: boolean
335+
}
336+
327337
/**
328338
* Libp2p nodes implement this interface.
329339
*/
@@ -608,12 +618,23 @@ export interface Libp2p<T extends ServiceMap = ServiceMap> extends Startable, Ty
608618
unregister(id: string): void
609619

610620
/**
611-
* Returns the public key for the passed PeerId. If the PeerId is of the 'RSA' type
612-
* this may mean searching the DHT if the key is not present in the KeyStore.
613-
* A set of user defined services
621+
* Returns the public key for the passed PeerId. If the PeerId is of the 'RSA'
622+
* type this may mean searching the routing if the peer's key is not present
623+
* in the peer store.
614624
*/
615625
getPublicKey(peer: PeerId, options?: AbortOptions): Promise<Uint8Array>
616626

627+
/**
628+
* Given the current node configuration, returns a promise of `true` or
629+
* `false` if the node would attempt to dial the passed multiaddr.
630+
*
631+
* This means a relevant transport is configured, and the connection gater
632+
* would not block the dial attempt.
633+
*
634+
* This may involve resolving DNS addresses so you should pass an AbortSignal.
635+
*/
636+
isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise<boolean>
637+
617638
/**
618639
* A set of user defined services
619640
*/

packages/libp2p/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"@libp2p/utils": "^5.2.8",
9898
"@multiformats/dns": "^1.0.5",
9999
"@multiformats/multiaddr": "^12.2.1",
100+
"@multiformats/multiaddr-matcher": "^1.2.0",
100101
"any-signal": "^4.1.1",
101102
"datastore-core": "^9.2.9",
102103
"interface-datastore": "^8.2.11",
@@ -116,7 +117,6 @@
116117
"@libp2p/tcp": "^9.0.19",
117118
"@libp2p/websockets": "^8.0.18",
118119
"@multiformats/mafmt": "^12.1.6",
119-
"@multiformats/multiaddr-matcher": "^1.2.0",
120120
"aegir": "^42.2.5",
121121
"delay": "^6.0.0",
122122
"it-all": "^3.0.4",

packages/libp2p/src/connection-manager/dial-queue.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { defaultAddressSort } from '@libp2p/utils/address-sort'
55
import { Queue, type QueueAddOptions } from '@libp2p/utils/queue'
66
import { type Multiaddr, type Resolver, resolvers, multiaddr } from '@multiformats/multiaddr'
77
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
8+
import { Circuit } from '@multiformats/multiaddr-matcher'
89
import { type ClearableSignal, anySignal } from 'any-signal'
910
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
1011
import { codes } from '../errors.js'
@@ -17,7 +18,7 @@ import {
1718
MAX_DIAL_QUEUE_LENGTH
1819
} from './constants.js'
1920
import { resolveMultiaddrs } from './utils.js'
20-
import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting } from '@libp2p/interface'
21+
import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting, IsDialableOptions } from '@libp2p/interface'
2122
import type { TransportManager } from '@libp2p/interface-internal'
2223
import type { DNS } from '@multiformats/dns'
2324

@@ -456,4 +457,27 @@ export class DialQueue {
456457

457458
return sortedGatedAddrs
458459
}
460+
461+
async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise<boolean> {
462+
if (!Array.isArray(multiaddr)) {
463+
multiaddr = [multiaddr]
464+
}
465+
466+
try {
467+
const addresses = await this.calculateMultiaddrs(undefined, new Set(multiaddr.map(ma => ma.toString())), options)
468+
469+
if (options.runOnTransientConnection === false) {
470+
// return true if any resolved multiaddrs are not relay addresses
471+
return addresses.find(addr => {
472+
return !Circuit.matches(addr.multiaddr)
473+
}) != null
474+
}
475+
476+
return true
477+
} catch (err) {
478+
this.log.trace('error calculating if multiaddr(s) were dialable', err)
479+
}
480+
481+
return false
482+
}
459483
}

packages/libp2p/src/connection-manager/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { AutoDial } from './auto-dial.js'
1010
import { ConnectionPruner } from './connection-pruner.js'
1111
import { AUTO_DIAL_CONCURRENCY, AUTO_DIAL_MAX_QUEUE_LENGTH, AUTO_DIAL_PRIORITY, DIAL_TIMEOUT, INBOUND_CONNECTION_THRESHOLD, MAX_CONNECTIONS, MAX_DIAL_QUEUE_LENGTH, MAX_INCOMING_PENDING_CONNECTIONS, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL, MIN_CONNECTIONS } from './constants.js'
1212
import { DialQueue } from './dial-queue.js'
13-
import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, Peer, PeerStore, Startable, PendingDialStatus, PeerRouting } from '@libp2p/interface'
13+
import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, Peer, PeerStore, Startable, PendingDialStatus, PeerRouting, IsDialableOptions } from '@libp2p/interface'
1414
import type { ConnectionManager, OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal'
1515
import type { JobStatus } from '@libp2p/utils/queue'
1616

@@ -177,7 +177,6 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
177177
public readonly autoDial: AutoDial
178178
public readonly connectionPruner: ConnectionPruner
179179
private readonly inboundConnectionRateLimiter: RateLimiter
180-
181180
private readonly peerStore: PeerStore
182181
private readonly metrics?: Metrics
183182
private readonly events: TypedEventTarget<Libp2pEvents>
@@ -621,4 +620,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
621620
}
622621
})
623622
}
623+
624+
async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise<boolean> {
625+
return this.dialQueue.isDialable(multiaddr, options)
626+
}
624627
}

packages/libp2p/src/libp2p.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { DefaultUpgrader } from './upgrader.js'
2323
import * as pkg from './version.js'
2424
import type { Components } from './components.js'
2525
import type { Libp2p, Libp2pInit, Libp2pOptions } from './index.js'
26-
import type { PeerRouting, ContentRouting, Libp2pEvents, PendingDial, ServiceMap, AbortOptions, ComponentLogger, Logger, Connection, NewStreamOptions, Stream, Metrics, PeerId, PeerInfo, PeerStore, Topology, Libp2pStatus } from '@libp2p/interface'
26+
import type { PeerRouting, ContentRouting, Libp2pEvents, PendingDial, ServiceMap, AbortOptions, ComponentLogger, Logger, Connection, NewStreamOptions, Stream, Metrics, PeerId, PeerInfo, PeerStore, Topology, Libp2pStatus, IsDialableOptions } from '@libp2p/interface'
2727
import type { StreamHandler, StreamHandlerOptions } from '@libp2p/interface-internal'
2828

2929
export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends TypedEventEmitter<Libp2pEvents> implements Libp2p<T> {
@@ -375,6 +375,10 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
375375
this.components.registrar.unregister(id)
376376
}
377377

378+
async isDialable (multiaddr: Multiaddr, options: IsDialableOptions = {}): Promise<boolean> {
379+
return this.components.connectionManager.isDialable(multiaddr, options)
380+
}
381+
378382
/**
379383
* Called whenever peer discovery services emit `peer` events and adds peers
380384
* to the peer store.
Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
/* eslint-env mocha */
22

3+
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
4+
import { webSockets } from '@libp2p/websockets'
5+
import { multiaddr } from '@multiformats/multiaddr'
36
import { expect } from 'aegir/chai'
47
import { createLibp2p } from '../../src/index.js'
58
import type { Libp2p } from '@libp2p/interface'
69

710
describe('core', () => {
811
let libp2p: Libp2p
912

10-
after(async () => {
13+
afterEach(async () => {
1114
await libp2p.stop()
1215
})
1316

@@ -16,4 +19,49 @@ describe('core', () => {
1619

1720
expect(libp2p).to.have.property('status', 'started')
1821
})
22+
23+
it('should say an address is not dialable if we have no transport for it', async () => {
24+
libp2p = await createLibp2p({
25+
transports: [
26+
webSockets()
27+
]
28+
})
29+
30+
const ma = multiaddr('/dns4/example.com/sctp/1234')
31+
32+
await expect(libp2p.isDialable(ma)).to.eventually.be.false()
33+
})
34+
35+
it('should say an address is dialable if a transport is configured', async () => {
36+
libp2p = await createLibp2p({
37+
transports: [
38+
webSockets()
39+
]
40+
})
41+
42+
const ma = multiaddr('/dns4/example.com/tls/ws')
43+
44+
await expect(libp2p.isDialable(ma)).to.eventually.be.true()
45+
})
46+
47+
it('should test if a protocol can run over a transient connection', async () => {
48+
libp2p = await createLibp2p({
49+
transports: [
50+
webSockets(),
51+
circuitRelayTransport()
52+
]
53+
})
54+
55+
await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws'), {
56+
runOnTransientConnection: false
57+
})).to.eventually.be.true()
58+
59+
await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), {
60+
runOnTransientConnection: true
61+
})).to.eventually.be.true()
62+
63+
await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), {
64+
runOnTransientConnection: false
65+
})).to.eventually.be.false()
66+
})
1967
})

0 commit comments

Comments
 (0)