Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: sort addresses by transport before dial #2731

Merged
merged 2 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/libp2p/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FaultTolerance, InvalidParametersError } from '@libp2p/interface'
import { defaultAddressSort } from '@libp2p/utils/address-sort'
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
import mergeOptions from 'merge-options'
import type { Libp2pInit } from './index.js'
Expand All @@ -16,8 +15,7 @@ const DefaultConfig: Libp2pInit = {
connectionManager: {
resolvers: {
dnsaddr: dnsaddrResolver
},
addressSorter: defaultAddressSort
}
},
transportManager: {
faultTolerance: FaultTolerance.FATAL_ALL
Expand Down
137 changes: 137 additions & 0 deletions packages/libp2p/src/connection-manager/address-sorter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
import { Circuit, WebSockets, WebSocketsSecure, WebRTC, WebRTCDirect, WebTransport, TCP } from '@multiformats/multiaddr-matcher'
import type { Address } from '@libp2p/interface'

/**
* Sorts addresses by order of reliability, where they have presented the fewest
* problems:
*
* TCP -> WebSockets/Secure -> WebRTC -> WebRTCDirect -> WebTransport
*/
// eslint-disable-next-line complexity
export function reliableTransportsFirst (a: Address, b: Address): -1 | 0 | 1 {
const isATCP = TCP.exactMatch(a.multiaddr)
const isBTCP = TCP.exactMatch(b.multiaddr)

if (isATCP && !isBTCP) {
return -1
}

if (!isATCP && isBTCP) {
return 1
}

const isAWebSocketSecure = WebSocketsSecure.exactMatch(a.multiaddr)
const isBWebSocketSecure = WebSocketsSecure.exactMatch(b.multiaddr)

if (isAWebSocketSecure && !isBWebSocketSecure) {
return -1

Check warning on line 28 in packages/libp2p/src/connection-manager/address-sorter.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/connection-manager/address-sorter.ts#L28

Added line #L28 was not covered by tests
}

if (!isAWebSocketSecure && isBWebSocketSecure) {
return 1
}

const isAWebSocket = WebSockets.exactMatch(a.multiaddr)
const isBWebSocket = WebSockets.exactMatch(b.multiaddr)

if (isAWebSocket && !isBWebSocket) {
return -1
}

if (!isAWebSocket && isBWebSocket) {
return 1
}

const isAWebRTC = WebRTC.exactMatch(a.multiaddr)
const isBWebRTC = WebRTC.exactMatch(b.multiaddr)

if (isAWebRTC && !isBWebRTC) {
return -1

Check warning on line 50 in packages/libp2p/src/connection-manager/address-sorter.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/connection-manager/address-sorter.ts#L50

Added line #L50 was not covered by tests
}

if (!isAWebRTC && isBWebRTC) {
return 1
}

const isAWebRTCDirect = WebRTCDirect.exactMatch(a.multiaddr)
const isBWebRTCDirect = WebRTCDirect.exactMatch(b.multiaddr)

if (isAWebRTCDirect && !isBWebRTCDirect) {
return -1

Check warning on line 61 in packages/libp2p/src/connection-manager/address-sorter.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/connection-manager/address-sorter.ts#L61

Added line #L61 was not covered by tests
}

if (!isAWebRTCDirect && isBWebRTCDirect) {
return 1
}

const isAWebTransport = WebTransport.exactMatch(a.multiaddr)
const isBWebTransport = WebTransport.exactMatch(b.multiaddr)

if (isAWebTransport && !isBWebTransport) {
return -1

Check warning on line 72 in packages/libp2p/src/connection-manager/address-sorter.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/connection-manager/address-sorter.ts#L72

Added line #L72 was not covered by tests
}

if (!isAWebTransport && isBWebTransport) {
return 1
}

// ... everything else
return 0
}

/**
* Compare function for array.sort() that moves public addresses to the start
* of the array.
*/
export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
const isAPrivate = isPrivate(a.multiaddr)
const isBPrivate = isPrivate(b.multiaddr)

if (isAPrivate && !isBPrivate) {
return 1
} else if (!isAPrivate && isBPrivate) {
return -1
}

return 0
}

/**
* Compare function for array.sort() that moves certified addresses to the start
* of the array.
*/
export function certifiedAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
if (a.isCertified && !b.isCertified) {
return -1
} else if (!a.isCertified && b.isCertified) {
return 1
}

return 0
}

/**
* Compare function for array.sort() that moves circuit relay addresses to the
* end of the array.
*/
export function circuitRelayAddressesLast (a: Address, b: Address): -1 | 0 | 1 {
const isACircuit = Circuit.exactMatch(a.multiaddr)
const isBCircuit = Circuit.exactMatch(b.multiaddr)

if (isACircuit && !isBCircuit) {
return 1
} else if (!isACircuit && isBCircuit) {
return -1
}

return 0
}

export function defaultAddressSorter (addresses: Address[]): Address[] {
return addresses
.sort(reliableTransportsFirst)
.sort(certifiedAddressesFirst)
.sort(circuitRelayAddressesLast)
.sort(publicAddressesFirst)
}
9 changes: 4 additions & 5 deletions packages/libp2p/src/connection-manager/dial-queue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable max-depth */
import { TimeoutError, DialError, setMaxListeners, AbortError } from '@libp2p/interface'
import { PeerMap } from '@libp2p/peer-collections'
import { defaultAddressSort } from '@libp2p/utils/address-sort'
import { PriorityQueue, type PriorityQueueJobOptions } from '@libp2p/utils/priority-queue'
import { type Multiaddr, type Resolver, resolvers, multiaddr } from '@multiformats/multiaddr'
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
Expand All @@ -11,6 +10,7 @@ import { CustomProgressEvent } from 'progress-events'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { DialDeniedError, NoValidAddressesError } from '../errors.js'
import { getPeerAddress } from '../get-peer.js'
import { defaultAddressSorter } from './address-sorter.js'
import {
DIAL_TIMEOUT,
MAX_PARALLEL_DIALS,
Expand Down Expand Up @@ -47,7 +47,6 @@ interface DialerInit {
}

const defaultOptions = {
addressSorter: defaultAddressSort,
maxParallelDials: MAX_PARALLEL_DIALS,
maxDialQueueLength: MAX_DIAL_QUEUE_LENGTH,
maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL,
Expand All @@ -71,7 +70,7 @@ interface DialQueueComponents {
export class DialQueue {
public queue: PriorityQueue<Connection, DialQueueJobOptions>
private readonly components: DialQueueComponents
private readonly addressSorter: AddressSorter
private readonly addressSorter?: AddressSorter
private readonly maxPeerAddrsToDial: number
private readonly maxDialQueueLength: number
private readonly dialTimeout: number
Expand All @@ -80,7 +79,7 @@ export class DialQueue {
private readonly log: Logger

constructor (components: DialQueueComponents, init: DialerInit = {}) {
this.addressSorter = init.addressSorter ?? defaultOptions.addressSorter
this.addressSorter = init.addressSorter
this.maxPeerAddrsToDial = init.maxPeerAddrsToDial ?? defaultOptions.maxPeerAddrsToDial
this.maxDialQueueLength = init.maxDialQueueLength ?? defaultOptions.maxDialQueueLength
this.dialTimeout = init.dialTimeout ?? defaultOptions.dialTimeout
Expand Down Expand Up @@ -467,7 +466,7 @@ export class DialQueue {
gatedAdrs.push(addr)
}

const sortedGatedAddrs = gatedAdrs.sort(this.addressSorter)
const sortedGatedAddrs = this.addressSorter == null ? defaultAddressSorter(gatedAdrs) : gatedAdrs.sort(this.addressSorter)

// make sure we actually have some addresses to dial
if (sortedGatedAddrs.length === 0) {
Expand Down
3 changes: 1 addition & 2 deletions packages/libp2p/src/connection-manager/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InvalidMultiaddrError, InvalidParametersError, InvalidPeerIdError, NotStartedError, start, stop } from '@libp2p/interface'
import { PeerMap } from '@libp2p/peer-collections'
import { defaultAddressSort } from '@libp2p/utils/address-sort'
import { RateLimiter } from '@libp2p/utils/rate-limiter'
import { type Multiaddr, type Resolver, multiaddr } from '@multiformats/multiaddr'
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
Expand Down Expand Up @@ -242,7 +241,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
})

this.dialQueue = new DialQueue(components, {
addressSorter: init.addressSorter ?? defaultAddressSort,
addressSorter: init.addressSorter,
maxParallelDials: init.maxParallelDials ?? MAX_PARALLEL_DIALS,
maxDialQueueLength: init.maxDialQueueLength ?? MAX_DIAL_QUEUE_LENGTH,
maxPeerAddrsToDial: init.maxPeerAddrsToDial ?? MAX_PEER_ADDRS_TO_DIAL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import { publicAddressesFirst, certifiedAddressesFirst, circuitRelayAddressesLast, defaultAddressSort } from '../src/address-sort.js'
import { defaultAddressSorter } from '../../src/connection-manager/address-sorter.js'

describe('address-sort', () => {
describe('public addresses first', () => {
Expand All @@ -21,7 +21,7 @@ describe('address-sort', () => {
publicAddress
]

const sortedAddresses = addresses.sort(publicAddressesFirst)
const sortedAddresses = defaultAddressSorter(addresses)
expect(sortedAddresses).to.deep.equal([
publicAddress,
privateAddress
Expand Down Expand Up @@ -55,11 +55,11 @@ describe('address-sort', () => {
privateAddress
]

const sortedAddresses = addresses.sort(certifiedAddressesFirst)
const sortedAddresses = defaultAddressSorter(addresses)
expect(sortedAddresses).to.deep.equal([
certifiedPublicAddress,
certifiedPrivateAddress,
publicAddress,
certifiedPrivateAddress,
privateAddress
])
})
Expand All @@ -81,7 +81,7 @@ describe('address-sort', () => {
publicAddress
]

const sortedAddresses = addresses.sort(circuitRelayAddressesLast)
const sortedAddresses = defaultAddressSorter(addresses)
expect(sortedAddresses).to.deep.equal([
publicAddress,
publicRelay
Expand Down Expand Up @@ -137,7 +137,8 @@ describe('address-sort', () => {
return Math.random() > 0.5 ? -1 : 1
})

const sortedAddresses = addresses.sort(defaultAddressSort)
const sortedAddresses = defaultAddressSorter(addresses)

expect(sortedAddresses).to.deep.equal([
certifiedPublicAddress,
publicAddress,
Expand Down Expand Up @@ -167,11 +168,41 @@ describe('address-sort', () => {
return Math.random() > 0.5 ? -1 : 1
})

const sortedAddresses = addresses.sort(defaultAddressSort)
const sortedAddresses = defaultAddressSorter(addresses)
expect(sortedAddresses).to.deep.equal([
webRTCOverRelay,
publicRelay
])
})

it('should sort reliable addresses first', () => {
const tcp = multiaddr('/ip4/123.123.123.123/tcp/123/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
const ws = multiaddr('/ip4/123.123.123.123/tcp/123/ws/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
const wss = multiaddr('/ip4/123.123.123.123/tcp/123/wss/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
const webRTC = multiaddr('/ip4/123.123.123.123/tcp/123/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/webrtc/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
const webRTCDirect = multiaddr('/ip4/123.123.123.123/udp/123/webrtc-direct/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
const circuitRelay = multiaddr('/ip4/123.123.123.123/tcp/123/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
const webTransport = multiaddr('/ip4/123.123.123.123/udp/123/quic-v1/webtransport/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN')

const addresses = [tcp, ws, wss, webRTC, webRTCDirect, circuitRelay, webTransport]
.sort(() => Math.random() < 0.5 ? -1 : 1)
.map(multiaddr => ({
multiaddr,
isCertified: true
}))

const sortedAddresses = defaultAddressSorter(addresses)
.map(({ multiaddr }) => multiaddr.toString())

expect(sortedAddresses).to.deep.equal([
tcp,
wss,
ws,
webRTC,
webRTCDirect,
webTransport,
circuitRelay
].map(ma => ma.toString()))
})
})
})
6 changes: 3 additions & 3 deletions packages/libp2p/test/connection-manager/direct.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { mplex } from '@libp2p/mplex'
import { peerIdFromString, peerIdFromPrivateKey } from '@libp2p/peer-id'
import { persistentPeerStore } from '@libp2p/peer-store'
import { plaintext } from '@libp2p/plaintext'
import { defaultAddressSort } from '@libp2p/utils/address-sort'
import { webSockets } from '@libp2p/websockets'
import * as filters from '@libp2p/websockets/filters'
import { multiaddr } from '@multiformats/multiaddr'
Expand Down Expand Up @@ -190,7 +189,8 @@ describe('dialing (direct, WebSockets)', () => {
multiaddr('/ip4/30.0.0.1/tcp/15001/ws')
]

const addressesSorttSpy = sinon.spy(defaultAddressSort)
const addressSorter = (): 0 => 0
const addressesSorttSpy = sinon.spy(addressSorter)
const localTMDialStub = sinon.stub(localTM, 'dial').callsFake(async (ma) => mockConnection(mockMultiaddrConnection(mockDuplex(), remoteComponents.peerId)))

connectionManager = new DefaultConnectionManager(localComponents, {
Expand All @@ -209,7 +209,7 @@ describe('dialing (direct, WebSockets)', () => {

const sortedAddresses = peerMultiaddrs
.map((m) => ({ multiaddr: m, isCertified: false }))
.sort(defaultAddressSort)
.sort(addressSorter)

expect(localTMDialStub.getCall(0).args[0].equals(sortedAddresses[0].multiaddr))
})
Expand Down
5 changes: 0 additions & 5 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@
"types": "./dist/src/adaptive-timeout.d.ts",
"import": "./dist/src/adaptive-timeout.js"
},
"./address-sort": {
"types": "./dist/src/address-sort.d.ts",
"import": "./dist/src/address-sort.js"
},
"./array-equals": {
"types": "./dist/src/array-equals.d.ts",
"import": "./dist/src/array-equals.js"
Expand Down Expand Up @@ -156,7 +152,6 @@
"@libp2p/interface": "^2.1.2",
"@libp2p/logger": "^5.1.0",
"@multiformats/multiaddr": "^12.2.3",
"@multiformats/multiaddr-matcher": "^1.2.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@types/murmurhash3js-revisited": "^3.0.3",
"any-signal": "^4.1.1",
Expand Down
Loading
Loading