Skip to content

Commit dad979f

Browse files
authored
fix: sort addresses by transport before dial (#2731)
Expands the pre-dial address sorting to sort connections by reliability and speed.
1 parent 7e4e6bd commit dad979f

File tree

9 files changed

+184
-117
lines changed

9 files changed

+184
-117
lines changed

packages/libp2p/src/config.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { FaultTolerance, InvalidParametersError } from '@libp2p/interface'
2-
import { defaultAddressSort } from '@libp2p/utils/address-sort'
32
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
43
import mergeOptions from 'merge-options'
54
import type { Libp2pInit } from './index.js'
@@ -16,8 +15,7 @@ const DefaultConfig: Libp2pInit = {
1615
connectionManager: {
1716
resolvers: {
1817
dnsaddr: dnsaddrResolver
19-
},
20-
addressSorter: defaultAddressSort
18+
}
2119
},
2220
transportManager: {
2321
faultTolerance: FaultTolerance.FATAL_ALL
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
2+
import { Circuit, WebSockets, WebSocketsSecure, WebRTC, WebRTCDirect, WebTransport, TCP } from '@multiformats/multiaddr-matcher'
3+
import type { Address } from '@libp2p/interface'
4+
5+
/**
6+
* Sorts addresses by order of reliability, where they have presented the fewest
7+
* problems:
8+
*
9+
* TCP -> WebSockets/Secure -> WebRTC -> WebRTCDirect -> WebTransport
10+
*/
11+
// eslint-disable-next-line complexity
12+
export function reliableTransportsFirst (a: Address, b: Address): -1 | 0 | 1 {
13+
const isATCP = TCP.exactMatch(a.multiaddr)
14+
const isBTCP = TCP.exactMatch(b.multiaddr)
15+
16+
if (isATCP && !isBTCP) {
17+
return -1
18+
}
19+
20+
if (!isATCP && isBTCP) {
21+
return 1
22+
}
23+
24+
const isAWebSocketSecure = WebSocketsSecure.exactMatch(a.multiaddr)
25+
const isBWebSocketSecure = WebSocketsSecure.exactMatch(b.multiaddr)
26+
27+
if (isAWebSocketSecure && !isBWebSocketSecure) {
28+
return -1
29+
}
30+
31+
if (!isAWebSocketSecure && isBWebSocketSecure) {
32+
return 1
33+
}
34+
35+
const isAWebSocket = WebSockets.exactMatch(a.multiaddr)
36+
const isBWebSocket = WebSockets.exactMatch(b.multiaddr)
37+
38+
if (isAWebSocket && !isBWebSocket) {
39+
return -1
40+
}
41+
42+
if (!isAWebSocket && isBWebSocket) {
43+
return 1
44+
}
45+
46+
const isAWebRTC = WebRTC.exactMatch(a.multiaddr)
47+
const isBWebRTC = WebRTC.exactMatch(b.multiaddr)
48+
49+
if (isAWebRTC && !isBWebRTC) {
50+
return -1
51+
}
52+
53+
if (!isAWebRTC && isBWebRTC) {
54+
return 1
55+
}
56+
57+
const isAWebRTCDirect = WebRTCDirect.exactMatch(a.multiaddr)
58+
const isBWebRTCDirect = WebRTCDirect.exactMatch(b.multiaddr)
59+
60+
if (isAWebRTCDirect && !isBWebRTCDirect) {
61+
return -1
62+
}
63+
64+
if (!isAWebRTCDirect && isBWebRTCDirect) {
65+
return 1
66+
}
67+
68+
const isAWebTransport = WebTransport.exactMatch(a.multiaddr)
69+
const isBWebTransport = WebTransport.exactMatch(b.multiaddr)
70+
71+
if (isAWebTransport && !isBWebTransport) {
72+
return -1
73+
}
74+
75+
if (!isAWebTransport && isBWebTransport) {
76+
return 1
77+
}
78+
79+
// ... everything else
80+
return 0
81+
}
82+
83+
/**
84+
* Compare function for array.sort() that moves public addresses to the start
85+
* of the array.
86+
*/
87+
export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
88+
const isAPrivate = isPrivate(a.multiaddr)
89+
const isBPrivate = isPrivate(b.multiaddr)
90+
91+
if (isAPrivate && !isBPrivate) {
92+
return 1
93+
} else if (!isAPrivate && isBPrivate) {
94+
return -1
95+
}
96+
97+
return 0
98+
}
99+
100+
/**
101+
* Compare function for array.sort() that moves certified addresses to the start
102+
* of the array.
103+
*/
104+
export function certifiedAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
105+
if (a.isCertified && !b.isCertified) {
106+
return -1
107+
} else if (!a.isCertified && b.isCertified) {
108+
return 1
109+
}
110+
111+
return 0
112+
}
113+
114+
/**
115+
* Compare function for array.sort() that moves circuit relay addresses to the
116+
* end of the array.
117+
*/
118+
export function circuitRelayAddressesLast (a: Address, b: Address): -1 | 0 | 1 {
119+
const isACircuit = Circuit.exactMatch(a.multiaddr)
120+
const isBCircuit = Circuit.exactMatch(b.multiaddr)
121+
122+
if (isACircuit && !isBCircuit) {
123+
return 1
124+
} else if (!isACircuit && isBCircuit) {
125+
return -1
126+
}
127+
128+
return 0
129+
}
130+
131+
export function defaultAddressSorter (addresses: Address[]): Address[] {
132+
return addresses
133+
.sort(reliableTransportsFirst)
134+
.sort(certifiedAddressesFirst)
135+
.sort(circuitRelayAddressesLast)
136+
.sort(publicAddressesFirst)
137+
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable max-depth */
22
import { TimeoutError, DialError, setMaxListeners, AbortError } from '@libp2p/interface'
33
import { PeerMap } from '@libp2p/peer-collections'
4-
import { defaultAddressSort } from '@libp2p/utils/address-sort'
54
import { PriorityQueue, type PriorityQueueJobOptions } from '@libp2p/utils/priority-queue'
65
import { type Multiaddr, type Resolver, resolvers, multiaddr } from '@multiformats/multiaddr'
76
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
@@ -11,6 +10,7 @@ import { CustomProgressEvent } from 'progress-events'
1110
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
1211
import { DialDeniedError, NoValidAddressesError } from '../errors.js'
1312
import { getPeerAddress } from '../get-peer.js'
13+
import { defaultAddressSorter } from './address-sorter.js'
1414
import {
1515
DIAL_TIMEOUT,
1616
MAX_PARALLEL_DIALS,
@@ -47,7 +47,6 @@ interface DialerInit {
4747
}
4848

4949
const defaultOptions = {
50-
addressSorter: defaultAddressSort,
5150
maxParallelDials: MAX_PARALLEL_DIALS,
5251
maxDialQueueLength: MAX_DIAL_QUEUE_LENGTH,
5352
maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL,
@@ -71,7 +70,7 @@ interface DialQueueComponents {
7170
export class DialQueue {
7271
public queue: PriorityQueue<Connection, DialQueueJobOptions>
7372
private readonly components: DialQueueComponents
74-
private readonly addressSorter: AddressSorter
73+
private readonly addressSorter?: AddressSorter
7574
private readonly maxPeerAddrsToDial: number
7675
private readonly maxDialQueueLength: number
7776
private readonly dialTimeout: number
@@ -80,7 +79,7 @@ export class DialQueue {
8079
private readonly log: Logger
8180

8281
constructor (components: DialQueueComponents, init: DialerInit = {}) {
83-
this.addressSorter = init.addressSorter ?? defaultOptions.addressSorter
82+
this.addressSorter = init.addressSorter
8483
this.maxPeerAddrsToDial = init.maxPeerAddrsToDial ?? defaultOptions.maxPeerAddrsToDial
8584
this.maxDialQueueLength = init.maxDialQueueLength ?? defaultOptions.maxDialQueueLength
8685
this.dialTimeout = init.dialTimeout ?? defaultOptions.dialTimeout
@@ -467,7 +466,7 @@ export class DialQueue {
467466
gatedAdrs.push(addr)
468467
}
469468

470-
const sortedGatedAddrs = gatedAdrs.sort(this.addressSorter)
469+
const sortedGatedAddrs = this.addressSorter == null ? defaultAddressSorter(gatedAdrs) : gatedAdrs.sort(this.addressSorter)
471470

472471
// make sure we actually have some addresses to dial
473472
if (sortedGatedAddrs.length === 0) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ConnectionClosedError, InvalidMultiaddrError, InvalidParametersError, InvalidPeerIdError, NotStartedError, start, stop } from '@libp2p/interface'
22
import { PeerMap } from '@libp2p/peer-collections'
3-
import { defaultAddressSort } from '@libp2p/utils/address-sort'
43
import { RateLimiter } from '@libp2p/utils/rate-limiter'
54
import { type Multiaddr, type Resolver, multiaddr } from '@multiformats/multiaddr'
65
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
@@ -240,7 +239,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
240239
})
241240

242241
this.dialQueue = new DialQueue(components, {
243-
addressSorter: init.addressSorter ?? defaultAddressSort,
242+
addressSorter: init.addressSorter,
244243
maxParallelDials: init.maxParallelDials ?? MAX_PARALLEL_DIALS,
245244
maxDialQueueLength: init.maxDialQueueLength ?? MAX_DIAL_QUEUE_LENGTH,
246245
maxPeerAddrsToDial: init.maxPeerAddrsToDial ?? MAX_PEER_ADDRS_TO_DIAL,

packages/utils/test/address-sort.spec.ts renamed to packages/libp2p/test/connection-manager/address-sorter.spec.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

77
describe('address-sort', () => {
88
describe('public addresses first', () => {
@@ -21,7 +21,7 @@ describe('address-sort', () => {
2121
publicAddress
2222
]
2323

24-
const sortedAddresses = addresses.sort(publicAddressesFirst)
24+
const sortedAddresses = defaultAddressSorter(addresses)
2525
expect(sortedAddresses).to.deep.equal([
2626
publicAddress,
2727
privateAddress
@@ -55,11 +55,11 @@ describe('address-sort', () => {
5555
privateAddress
5656
]
5757

58-
const sortedAddresses = addresses.sort(certifiedAddressesFirst)
58+
const sortedAddresses = defaultAddressSorter(addresses)
5959
expect(sortedAddresses).to.deep.equal([
6060
certifiedPublicAddress,
61-
certifiedPrivateAddress,
6261
publicAddress,
62+
certifiedPrivateAddress,
6363
privateAddress
6464
])
6565
})
@@ -81,7 +81,7 @@ describe('address-sort', () => {
8181
publicAddress
8282
]
8383

84-
const sortedAddresses = addresses.sort(circuitRelayAddressesLast)
84+
const sortedAddresses = defaultAddressSorter(addresses)
8585
expect(sortedAddresses).to.deep.equal([
8686
publicAddress,
8787
publicRelay
@@ -137,7 +137,8 @@ describe('address-sort', () => {
137137
return Math.random() > 0.5 ? -1 : 1
138138
})
139139

140-
const sortedAddresses = addresses.sort(defaultAddressSort)
140+
const sortedAddresses = defaultAddressSorter(addresses)
141+
141142
expect(sortedAddresses).to.deep.equal([
142143
certifiedPublicAddress,
143144
publicAddress,
@@ -167,11 +168,41 @@ describe('address-sort', () => {
167168
return Math.random() > 0.5 ? -1 : 1
168169
})
169170

170-
const sortedAddresses = addresses.sort(defaultAddressSort)
171+
const sortedAddresses = defaultAddressSorter(addresses)
171172
expect(sortedAddresses).to.deep.equal([
172173
webRTCOverRelay,
173174
publicRelay
174175
])
175176
})
177+
178+
it('should sort reliable addresses first', () => {
179+
const tcp = multiaddr('/ip4/123.123.123.123/tcp/123/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
180+
const ws = multiaddr('/ip4/123.123.123.123/tcp/123/ws/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
181+
const wss = multiaddr('/ip4/123.123.123.123/tcp/123/wss/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
182+
const webRTC = multiaddr('/ip4/123.123.123.123/tcp/123/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/webrtc/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
183+
const webRTCDirect = multiaddr('/ip4/123.123.123.123/udp/123/webrtc-direct/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
184+
const circuitRelay = multiaddr('/ip4/123.123.123.123/tcp/123/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm')
185+
const webTransport = multiaddr('/ip4/123.123.123.123/udp/123/quic-v1/webtransport/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN')
186+
187+
const addresses = [tcp, ws, wss, webRTC, webRTCDirect, circuitRelay, webTransport]
188+
.sort(() => Math.random() < 0.5 ? -1 : 1)
189+
.map(multiaddr => ({
190+
multiaddr,
191+
isCertified: true
192+
}))
193+
194+
const sortedAddresses = defaultAddressSorter(addresses)
195+
.map(({ multiaddr }) => multiaddr.toString())
196+
197+
expect(sortedAddresses).to.deep.equal([
198+
tcp,
199+
wss,
200+
ws,
201+
webRTC,
202+
webRTCDirect,
203+
webTransport,
204+
circuitRelay
205+
].map(ma => ma.toString()))
206+
})
176207
})
177208
})

packages/libp2p/test/connection-manager/direct.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { mplex } from '@libp2p/mplex'
1010
import { peerIdFromString, peerIdFromPrivateKey } from '@libp2p/peer-id'
1111
import { persistentPeerStore } from '@libp2p/peer-store'
1212
import { plaintext } from '@libp2p/plaintext'
13-
import { defaultAddressSort } from '@libp2p/utils/address-sort'
1413
import { webSockets } from '@libp2p/websockets'
1514
import * as filters from '@libp2p/websockets/filters'
1615
import { multiaddr } from '@multiformats/multiaddr'
@@ -190,7 +189,8 @@ describe('dialing (direct, WebSockets)', () => {
190189
multiaddr('/ip4/30.0.0.1/tcp/15001/ws')
191190
]
192191

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

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

210210
const sortedAddresses = peerMultiaddrs
211211
.map((m) => ({ multiaddr: m, isCertified: false }))
212-
.sort(defaultAddressSort)
212+
.sort(addressSorter)
213213

214214
expect(localTMDialStub.getCall(0).args[0].equals(sortedAddresses[0].multiaddr))
215215
})

packages/utils/package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@
5656
"types": "./dist/src/adaptive-timeout.d.ts",
5757
"import": "./dist/src/adaptive-timeout.js"
5858
},
59-
"./address-sort": {
60-
"types": "./dist/src/address-sort.d.ts",
61-
"import": "./dist/src/address-sort.js"
62-
},
6359
"./array-equals": {
6460
"types": "./dist/src/array-equals.d.ts",
6561
"import": "./dist/src/array-equals.js"
@@ -156,7 +152,6 @@
156152
"@libp2p/interface": "^2.1.2",
157153
"@libp2p/logger": "^5.1.0",
158154
"@multiformats/multiaddr": "^12.2.3",
159-
"@multiformats/multiaddr-matcher": "^1.2.1",
160155
"@sindresorhus/fnv1a": "^3.1.0",
161156
"@types/murmurhash3js-revisited": "^3.0.3",
162157
"any-signal": "^4.1.1",

0 commit comments

Comments
 (0)