Skip to content

Commit

Permalink
feat: add isDialable method to libp2p (#2479)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
achingbrain authored Apr 12, 2024
1 parent c5003d4 commit 2c56203
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ class MockConnectionManager implements ConnectionManager, Startable {
getDialQueue (): PendingDial[] {
return []
}

async isDialable (): Promise<boolean> {
return true
}
}

export function mockConnectionManager (components: MockConnectionManagerComponents): ConnectionManager {
Expand Down
13 changes: 12 additions & 1 deletion packages/interface-internal/src/connection-manager/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AbortOptions, PendingDial, Connection, MultiaddrConnection, PeerId } from '@libp2p/interface'
import type { AbortOptions, PendingDial, Connection, MultiaddrConnection, PeerId, IsDialableOptions } from '@libp2p/interface'
import type { PeerMap } from '@libp2p/peer-collections'
import type { Multiaddr } from '@multiformats/multiaddr'

Expand Down Expand Up @@ -80,4 +80,15 @@ export interface ConnectionManager {
* ```
*/
getDialQueue(): PendingDial[]

/**
* Given the current node configuration, returns a promise of `true` or
* `false` if the node would attempt to dial the passed multiaddr.
*
* This means a relevant transport is configured, and the connection gater
* would not block the dial attempt.
*
* This may involve resolving DNS addresses so you should pass an AbortSignal.
*/
isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise<boolean>
}
27 changes: 24 additions & 3 deletions packages/interface/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,16 @@ export interface PendingDial {

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

export interface IsDialableOptions extends AbortOptions {
/**
* If the dial attempt would open a protocol, and the multiaddr being dialed
* is a circuit relay address, passing true here would cause the test to fail
* because that protocol would not be allowed to run over a data/time limited
* connection.
*/
runOnTransientConnection?: boolean
}

/**
* Libp2p nodes implement this interface.
*/
Expand Down Expand Up @@ -608,12 +618,23 @@ export interface Libp2p<T extends ServiceMap = ServiceMap> extends Startable, Ty
unregister(id: string): void

/**
* Returns the public key for the passed PeerId. If the PeerId is of the 'RSA' type
* this may mean searching the DHT if the key is not present in the KeyStore.
* A set of user defined services
* Returns the public key for the passed PeerId. If the PeerId is of the 'RSA'
* type this may mean searching the routing if the peer's key is not present
* in the peer store.
*/
getPublicKey(peer: PeerId, options?: AbortOptions): Promise<Uint8Array>

/**
* Given the current node configuration, returns a promise of `true` or
* `false` if the node would attempt to dial the passed multiaddr.
*
* This means a relevant transport is configured, and the connection gater
* would not block the dial attempt.
*
* This may involve resolving DNS addresses so you should pass an AbortSignal.
*/
isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise<boolean>

/**
* A set of user defined services
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/libp2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"@libp2p/utils": "^5.2.8",
"@multiformats/dns": "^1.0.5",
"@multiformats/multiaddr": "^12.2.1",
"@multiformats/multiaddr-matcher": "^1.2.0",
"any-signal": "^4.1.1",
"datastore-core": "^9.2.9",
"interface-datastore": "^8.2.11",
Expand All @@ -116,7 +117,6 @@
"@libp2p/tcp": "^9.0.19",
"@libp2p/websockets": "^8.0.18",
"@multiformats/mafmt": "^12.1.6",
"@multiformats/multiaddr-matcher": "^1.2.0",
"aegir": "^42.2.5",
"delay": "^6.0.0",
"it-all": "^3.0.4",
Expand Down
26 changes: 25 additions & 1 deletion packages/libp2p/src/connection-manager/dial-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { defaultAddressSort } from '@libp2p/utils/address-sort'
import { Queue, type QueueAddOptions } from '@libp2p/utils/queue'
import { type Multiaddr, type Resolver, resolvers, multiaddr } from '@multiformats/multiaddr'
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
import { Circuit } from '@multiformats/multiaddr-matcher'
import { type ClearableSignal, anySignal } from 'any-signal'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { codes } from '../errors.js'
Expand All @@ -17,7 +18,7 @@ import {
MAX_DIAL_QUEUE_LENGTH
} from './constants.js'
import { resolveMultiaddrs } from './utils.js'
import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting } from '@libp2p/interface'
import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting, IsDialableOptions } from '@libp2p/interface'
import type { TransportManager } from '@libp2p/interface-internal'
import type { DNS } from '@multiformats/dns'

Expand Down Expand Up @@ -456,4 +457,27 @@ export class DialQueue {

return sortedGatedAddrs
}

async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise<boolean> {
if (!Array.isArray(multiaddr)) {
multiaddr = [multiaddr]
}

try {
const addresses = await this.calculateMultiaddrs(undefined, new Set(multiaddr.map(ma => ma.toString())), options)

if (options.runOnTransientConnection === false) {
// return true if any resolved multiaddrs are not relay addresses
return addresses.find(addr => {
return !Circuit.matches(addr.multiaddr)
}) != null
}

return true
} catch (err) {
this.log.trace('error calculating if multiaddr(s) were dialable', err)
}

return false
}
}
7 changes: 5 additions & 2 deletions packages/libp2p/src/connection-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AutoDial } from './auto-dial.js'
import { ConnectionPruner } from './connection-pruner.js'
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'
import { DialQueue } from './dial-queue.js'
import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, Peer, PeerStore, Startable, PendingDialStatus, PeerRouting } from '@libp2p/interface'
import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, Peer, PeerStore, Startable, PendingDialStatus, PeerRouting, IsDialableOptions } from '@libp2p/interface'
import type { ConnectionManager, OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal'
import type { JobStatus } from '@libp2p/utils/queue'

Expand Down Expand Up @@ -177,7 +177,6 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
public readonly autoDial: AutoDial
public readonly connectionPruner: ConnectionPruner
private readonly inboundConnectionRateLimiter: RateLimiter

private readonly peerStore: PeerStore
private readonly metrics?: Metrics
private readonly events: TypedEventTarget<Libp2pEvents>
Expand Down Expand Up @@ -621,4 +620,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
}
})
}

async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise<boolean> {
return this.dialQueue.isDialable(multiaddr, options)
}
}
6 changes: 5 additions & 1 deletion packages/libp2p/src/libp2p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { DefaultUpgrader } from './upgrader.js'
import * as pkg from './version.js'
import type { Components } from './components.js'
import type { Libp2p, Libp2pInit, Libp2pOptions } from './index.js'
import type { PeerRouting, ContentRouting, Libp2pEvents, PendingDial, ServiceMap, AbortOptions, ComponentLogger, Logger, Connection, NewStreamOptions, Stream, Metrics, PeerId, PeerInfo, PeerStore, Topology, Libp2pStatus } from '@libp2p/interface'
import type { PeerRouting, ContentRouting, Libp2pEvents, PendingDial, ServiceMap, AbortOptions, ComponentLogger, Logger, Connection, NewStreamOptions, Stream, Metrics, PeerId, PeerInfo, PeerStore, Topology, Libp2pStatus, IsDialableOptions } from '@libp2p/interface'
import type { StreamHandler, StreamHandlerOptions } from '@libp2p/interface-internal'

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

async isDialable (multiaddr: Multiaddr, options: IsDialableOptions = {}): Promise<boolean> {
return this.components.connectionManager.isDialable(multiaddr, options)
}

/**
* Called whenever peer discovery services emit `peer` events and adds peers
* to the peer store.
Expand Down
50 changes: 49 additions & 1 deletion packages/libp2p/test/core/core.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/* eslint-env mocha */

import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { webSockets } from '@libp2p/websockets'
import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import { createLibp2p } from '../../src/index.js'
import type { Libp2p } from '@libp2p/interface'

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

after(async () => {
afterEach(async () => {
await libp2p.stop()
})

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

expect(libp2p).to.have.property('status', 'started')
})

it('should say an address is not dialable if we have no transport for it', async () => {
libp2p = await createLibp2p({
transports: [
webSockets()
]
})

const ma = multiaddr('/dns4/example.com/sctp/1234')

await expect(libp2p.isDialable(ma)).to.eventually.be.false()
})

it('should say an address is dialable if a transport is configured', async () => {
libp2p = await createLibp2p({
transports: [
webSockets()
]
})

const ma = multiaddr('/dns4/example.com/tls/ws')

await expect(libp2p.isDialable(ma)).to.eventually.be.true()
})

it('should test if a protocol can run over a transient connection', async () => {
libp2p = await createLibp2p({
transports: [
webSockets(),
circuitRelayTransport()
]
})

await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws'), {
runOnTransientConnection: false
})).to.eventually.be.true()

await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), {
runOnTransientConnection: true
})).to.eventually.be.true()

await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), {
runOnTransientConnection: false
})).to.eventually.be.false()
})
})

0 comments on commit 2c56203

Please sign in to comment.