Skip to content

Commit 5294f14

Browse files
authored
fix(libp2p): sort addresses to dial as public, then relay (#2031)
Adds a new default address sorter that sorts by: 1. public addresses 2. public relay addresess 3. private addresses 4. private relay addresses Where they are equal, certified addresses take priority, otherwise the sort order is stable.
1 parent 73b87c5 commit 5294f14

File tree

7 files changed

+208
-45
lines changed

7 files changed

+208
-45
lines changed

packages/libp2p/src/config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CodeError } from '@libp2p/interface/errors'
22
import { FaultTolerance } from '@libp2p/interface/transport'
3-
import { publicAddressesFirst } from '@libp2p/utils/address-sort'
3+
import { defaultAddressSort } from '@libp2p/utils/address-sort'
44
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
55
import mergeOptions from 'merge-options'
66
import { codes, messages } from './errors.js'
@@ -19,7 +19,7 @@ const DefaultConfig: Partial<Libp2pInit> = {
1919
resolvers: {
2020
dnsaddr: dnsaddrResolver
2121
},
22-
addressSorter: publicAddressesFirst
22+
addressSorter: defaultAddressSort
2323
},
2424
transportManager: {
2525
faultTolerance: FaultTolerance.FATAL_ALL

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { setMaxListeners } from 'events'
22
import { AbortError, CodeError } from '@libp2p/interface/errors'
33
import { logger } from '@libp2p/logger'
4-
import { publicAddressesFirst } from '@libp2p/utils/address-sort'
4+
import { defaultAddressSort } from '@libp2p/utils/address-sort'
55
import { type Multiaddr, type Resolver, resolvers } from '@multiformats/multiaddr'
66
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
77
import { type ClearableSignal, anySignal } from 'any-signal'
@@ -51,7 +51,7 @@ interface DialerInit {
5151
}
5252

5353
const defaultOptions = {
54-
addressSorter: publicAddressesFirst,
54+
addressSorter: defaultAddressSort,
5555
maxParallelDials: MAX_PARALLEL_DIALS,
5656
maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL,
5757
maxParallelDialsPerPeer: MAX_PARALLEL_DIALS_PER_PEER,

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CodeError } from '@libp2p/interface/errors'
22
import { KEEP_ALIVE } from '@libp2p/interface/peer-store/tags'
33
import { logger } from '@libp2p/logger'
44
import { PeerMap } from '@libp2p/peer-collections'
5-
import { publicAddressesFirst } from '@libp2p/utils/address-sort'
5+
import { defaultAddressSort } from '@libp2p/utils/address-sort'
66
import { type Multiaddr, type Resolver, multiaddr } from '@multiformats/multiaddr'
77
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
88
import { RateLimiterMemory } from 'rate-limiter-flexible'
@@ -254,7 +254,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
254254
transportManager: components.transportManager,
255255
connectionGater: components.connectionGater
256256
}, {
257-
addressSorter: init.addressSorter ?? publicAddressesFirst,
257+
addressSorter: init.addressSorter ?? defaultAddressSort,
258258
maxParallelDials: init.maxParallelDials ?? MAX_PARALLEL_DIALS,
259259
maxPeerAddrsToDial: init.maxPeerAddrsToDial ?? MAX_PEER_ADDRS_TO_DIAL,
260260
maxParallelDialsPerPeer: init.maxParallelDialsPerPeer ?? MAX_PARALLEL_DIALS_PER_PEER,

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { mplex } from '@libp2p/mplex'
88
import { peerIdFromString } from '@libp2p/peer-id'
99
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
1010
import { PersistentPeerStore } from '@libp2p/peer-store'
11-
import { publicAddressesFirst } from '@libp2p/utils/address-sort'
11+
import { defaultAddressSort } from '@libp2p/utils/address-sort'
1212
import { webSockets } from '@libp2p/websockets'
1313
import * as filters from '@libp2p/websockets/filters'
1414
import { multiaddr } from '@multiformats/multiaddr'
@@ -193,11 +193,11 @@ describe('dialing (direct, WebSockets)', () => {
193193
multiaddr('/ip4/30.0.0.1/tcp/15001/ws')
194194
]
195195

196-
const publicAddressesFirstSpy = sinon.spy(publicAddressesFirst)
196+
const addressesSorttSpy = sinon.spy(defaultAddressSort)
197197
const localTMDialStub = sinon.stub(localTM, 'dial').callsFake(async (ma) => mockConnection(mockMultiaddrConnection(mockDuplex(), remoteComponents.peerId)))
198198

199199
connectionManager = new DefaultConnectionManager(localComponents, {
200-
addressSorter: publicAddressesFirstSpy,
200+
addressSorter: addressesSorttSpy,
201201
maxParallelDials: 3,
202202
maxParallelDialsPerPeer: 3
203203
})
@@ -213,7 +213,7 @@ describe('dialing (direct, WebSockets)', () => {
213213

214214
const sortedAddresses = peerMultiaddrs
215215
.map((m) => ({ multiaddr: m, isCertified: false }))
216-
.sort(publicAddressesFirst)
216+
.sort(defaultAddressSort)
217217

218218
expect(localTMDialStub.getCall(0).args[0].equals(sortedAddresses[0].multiaddr))
219219
expect(localTMDialStub.getCall(1).args[0].equals(sortedAddresses[1].multiaddr))

packages/utils/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@libp2p/interface": "^0.1.2",
9090
"@libp2p/logger": "^3.0.2",
9191
"@multiformats/multiaddr": "^12.1.5",
92+
"@multiformats/multiaddr-matcher": "^1.0.1",
9293
"is-loopback-addr": "^2.0.1",
9394
"it-stream-types": "^2.0.1",
9495
"private-ip": "^3.0.0",

packages/utils/src/address-sort.ts

+43-7
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
* ```
2121
*/
2222

23+
import { Circuit } from '@multiformats/multiaddr-matcher'
2324
import { isPrivate } from './multiaddr/is-private.js'
2425
import type { Address } from '@libp2p/interface/peer-store'
2526

2627
/**
27-
* Compare function for array.sort().
28-
* This sort aims to move the private addresses to the end of the array.
29-
* In case of equality, a certified address will come first.
28+
* Compare function for array.sort() that moves public addresses to the start
29+
* of the array.
3030
*/
3131
export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
3232
const isAPrivate = isPrivate(a.multiaddr)
@@ -37,7 +37,15 @@ export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
3737
} else if (!isAPrivate && isBPrivate) {
3838
return -1
3939
}
40-
// Check certified?
40+
41+
return 0
42+
}
43+
44+
/**
45+
* Compare function for array.sort() that moves certified addresses to the start
46+
* of the array.
47+
*/
48+
export function certifiedAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
4149
if (a.isCertified && !b.isCertified) {
4250
return -1
4351
} else if (!a.isCertified && b.isCertified) {
@@ -48,8 +56,36 @@ export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 {
4856
}
4957

5058
/**
51-
* A test thing
59+
* Compare function for array.sort() that moves circuit relay addresses to the
60+
* start of the array.
5261
*/
53-
export async function something (): Promise<Uint8Array> {
54-
return Uint8Array.from([0, 1, 2])
62+
export function circuitRelayAddressesLast (a: Address, b: Address): -1 | 0 | 1 {
63+
const isACircuit = Circuit.exactMatch(a.multiaddr)
64+
const isBCircuit = Circuit.exactMatch(b.multiaddr)
65+
66+
if (isACircuit && !isBCircuit) {
67+
return 1
68+
} else if (!isACircuit && isBCircuit) {
69+
return -1
70+
}
71+
72+
return 0
73+
}
74+
75+
export function defaultAddressSort (a: Address, b: Address): -1 | 0 | 1 {
76+
const publicResult = publicAddressesFirst(a, b)
77+
78+
if (publicResult !== 0) {
79+
return publicResult
80+
}
81+
82+
const relayResult = circuitRelayAddressesLast(a, b)
83+
84+
if (relayResult !== 0) {
85+
return relayResult
86+
}
87+
88+
const certifiedResult = certifiedAddressesFirst(a, b)
89+
90+
return certifiedResult
5591
}

packages/utils/test/address-sort.spec.ts

+154-28
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,176 @@
22

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

77
describe('address-sort', () => {
8-
it('should sort public addresses first', () => {
9-
const addresses = [
10-
{
8+
describe('public addresses first', () => {
9+
it('should sort public addresses first', () => {
10+
const publicAddress = {
11+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'),
12+
isCertified: false
13+
}
14+
const privateAddress = {
1115
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'),
1216
isCertified: false
13-
},
14-
{
17+
}
18+
19+
const addresses = [
20+
privateAddress,
21+
publicAddress
22+
]
23+
24+
const sortedAddresses = addresses.sort(publicAddressesFirst)
25+
expect(sortedAddresses).to.deep.equal([
26+
publicAddress,
27+
privateAddress
28+
])
29+
})
30+
})
31+
32+
describe('certified addresses first', () => {
33+
it('should sort certified addresses first', () => {
34+
const certifiedPublicAddress = {
35+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4001'),
36+
isCertified: true
37+
}
38+
const publicAddress = {
1539
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'),
1640
isCertified: false
17-
},
18-
{
19-
multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'),
41+
}
42+
const certifiedPrivateAddress = {
43+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'),
44+
isCertified: true
45+
}
46+
const privateAddress = {
47+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'),
2048
isCertified: false
2149
}
22-
]
2350

24-
const sortedAddresses = addresses.sort(publicAddressesFirst)
25-
expect(sortedAddresses[0].multiaddr.equals(multiaddr('/ip4/30.0.0.1/tcp/4000'))).to.eql(true)
26-
expect(sortedAddresses[1].multiaddr.equals(multiaddr('/ip4/31.0.0.1/tcp/4000'))).to.eql(true)
27-
expect(sortedAddresses[2].multiaddr.equals(multiaddr('/ip4/127.0.0.1/tcp/4000'))).to.eql(true)
51+
const addresses = [
52+
publicAddress,
53+
certifiedPublicAddress,
54+
certifiedPrivateAddress,
55+
privateAddress
56+
]
57+
58+
const sortedAddresses = addresses.sort(certifiedAddressesFirst)
59+
expect(sortedAddresses).to.deep.equal([
60+
certifiedPublicAddress,
61+
certifiedPrivateAddress,
62+
publicAddress,
63+
privateAddress
64+
])
65+
})
2866
})
2967

30-
it('should sort public certified addresses first', () => {
31-
const addresses = [
32-
{
33-
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'),
68+
describe('circuit relay addresses last', () => {
69+
it('should sort circuit relay addresses last', () => {
70+
const publicAddress = {
71+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'),
3472
isCertified: false
35-
},
36-
{
73+
}
74+
const publicRelay = {
75+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'),
76+
isCertified: false
77+
}
78+
79+
const addresses = [
80+
publicRelay,
81+
publicAddress
82+
]
83+
84+
const sortedAddresses = addresses.sort(circuitRelayAddressesLast)
85+
expect(sortedAddresses).to.deep.equal([
86+
publicAddress,
87+
publicRelay
88+
])
89+
})
90+
})
91+
92+
describe('default address sort', () => {
93+
it('should sort public, then public relay, then private, then private relay with certified addresses taking priority', () => {
94+
const certifiedPublicAddress = {
95+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4001'),
96+
isCertified: true
97+
}
98+
const publicAddress = {
3799
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'),
38100
isCertified: false
39-
},
40-
{
41-
multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'),
101+
}
102+
const certifiedPublicRelay = {
103+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'),
104+
isCertified: true
105+
}
106+
const publicRelay = {
107+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'),
108+
isCertified: false
109+
}
110+
const certifiedPrivateAddress = {
111+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'),
42112
isCertified: true
43113
}
44-
]
114+
const privateAddress = {
115+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'),
116+
isCertified: false
117+
}
118+
const certifiedPrivateRelay = {
119+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'),
120+
isCertified: true
121+
}
122+
const privateRelay = {
123+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'),
124+
isCertified: false
125+
}
126+
127+
const addresses = [
128+
privateAddress,
129+
certifiedPrivateAddress,
130+
publicRelay,
131+
certifiedPublicRelay,
132+
privateRelay,
133+
publicAddress,
134+
certifiedPublicAddress,
135+
certifiedPrivateRelay
136+
].sort(() => {
137+
return Math.random() > 0.5 ? -1 : 1
138+
})
139+
140+
const sortedAddresses = addresses.sort(defaultAddressSort)
141+
expect(sortedAddresses).to.deep.equal([
142+
certifiedPublicAddress,
143+
publicAddress,
144+
certifiedPublicRelay,
145+
publicRelay,
146+
certifiedPrivateAddress,
147+
privateAddress,
148+
certifiedPrivateRelay,
149+
privateRelay
150+
])
151+
})
152+
153+
it('should sort WebRTC over relay addresses before relay addresses', () => {
154+
const publicRelay = {
155+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'),
156+
isCertified: false
157+
}
158+
const webRTCOverRelay = {
159+
multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm/webrtc'),
160+
isCertified: false
161+
}
162+
163+
const addresses = [
164+
publicRelay,
165+
webRTCOverRelay
166+
].sort(() => {
167+
return Math.random() > 0.5 ? -1 : 1
168+
})
45169

46-
const sortedAddresses = addresses.sort(publicAddressesFirst)
47-
expect(sortedAddresses[0].multiaddr.equals(multiaddr('/ip4/31.0.0.1/tcp/4000'))).to.eql(true)
48-
expect(sortedAddresses[1].multiaddr.equals(multiaddr('/ip4/30.0.0.1/tcp/4000'))).to.eql(true)
49-
expect(sortedAddresses[2].multiaddr.equals(multiaddr('/ip4/127.0.0.1/tcp/4000'))).to.eql(true)
170+
const sortedAddresses = addresses.sort(defaultAddressSort)
171+
expect(sortedAddresses).to.deep.equal([
172+
webRTCOverRelay,
173+
publicRelay
174+
])
175+
})
50176
})
51177
})

0 commit comments

Comments
 (0)