Skip to content

Commit 67af898

Browse files
committed
fix: append peer id to circuit relay addresses in peer:discovery event
1 parent f87cba9 commit 67af898

File tree

3 files changed

+268
-4
lines changed

3 files changed

+268
-4
lines changed

packages/libp2p/src/libp2p.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { defaultLogger } from '@libp2p/logger'
44
import { PeerSet } from '@libp2p/peer-collections'
55
import { peerIdFromString } from '@libp2p/peer-id'
66
import { persistentPeerStore } from '@libp2p/peer-store'
7-
import { CODE_P2P, isMultiaddr } from '@multiformats/multiaddr'
7+
import { CODE_P2P, CODE_P2P_CIRCUIT, isMultiaddr, multiaddr } from '@multiformats/multiaddr'
88
import { MemoryDatastore } from 'datastore-core/memory'
99
import { TypedEventEmitter, setMaxListeners } from 'main-event'
1010
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
@@ -100,9 +100,24 @@ export class Libp2p<T extends ServiceMap = ServiceMap> extends TypedEventEmitter
100100
components.events.addEventListener('peer:update', evt => {
101101
// if there was no peer previously in the peer store this is a new peer
102102
if (evt.detail.previous == null) {
103+
const id = evt.detail.peer.id
103104
const peerInfo: PeerInfo = {
104-
id: evt.detail.peer.id,
105-
multiaddrs: evt.detail.peer.addresses.map(a => a.multiaddr)
105+
id,
106+
multiaddrs: evt.detail.peer.addresses.map(a => {
107+
const ma = a.multiaddr
108+
const components = ma.getComponents()
109+
const circuitIdx = components.findIndex(c => c.code === CODE_P2P_CIRCUIT)
110+
111+
// For circuit relay addresses, ensure the target peer ID is appended
112+
// so callers can dial the address directly without having to add it manually.
113+
// Peers often announce relay addresses without their own peer ID (e.g. from
114+
// identify), so we add it here since it is known from the peer store.
115+
if (circuitIdx !== -1 && !components.slice(circuitIdx + 1).some(c => c.code === CODE_P2P)) {
116+
return ma.encapsulate(multiaddr(`/p2p/${id}`))
117+
}
118+
119+
return ma
120+
})
106121
}
107122

108123
components.events.safeDispatchEvent('peer:discovery', { detail: peerInfo })

packages/libp2p/test/peer-discovery/peer-discovery.spec.ts

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { generateKeyPair } from '@libp2p/crypto/keys'
2+
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
13
import { multiaddr } from '@multiformats/multiaddr'
24
import { expect } from 'aegir/chai'
35
import { TypedEventEmitter } from 'main-event'
6+
import { pEvent } from 'p-event'
47
import sinon from 'sinon'
58
import { stubInterface } from 'sinon-ts'
69
import { createLibp2p } from '../../src/index.js'
7-
import type { PeerDiscovery, PeerDiscoveryEvents, Startable, Libp2p } from '@libp2p/interface'
10+
import type { PeerDiscovery, PeerDiscoveryEvents, PeerInfo, Startable, Libp2p } from '@libp2p/interface'
811

912
describe('peer discovery', () => {
1013
let libp2p: Libp2p
@@ -33,6 +36,202 @@ describe('peer discovery', () => {
3336
expect(discovery.stop.calledOnce).to.be.true()
3437
})
3538

39+
it('should append peer id to circuit relay addresses in peer:discovery event', async () => {
40+
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
41+
42+
libp2p = await createLibp2p({
43+
peerDiscovery: [() => discovery]
44+
})
45+
46+
await libp2p.start()
47+
48+
const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
49+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
50+
51+
// Address without target peer ID (e.g. as announced via identify by the remote peer)
52+
const relayAddr = multiaddr(`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit`)
53+
54+
const eventPromise = pEvent<'peer:discovery', CustomEvent<PeerInfo>>(libp2p, 'peer:discovery')
55+
56+
discovery.safeDispatchEvent('peer', {
57+
detail: {
58+
id: remotePeerId,
59+
multiaddrs: [relayAddr]
60+
}
61+
})
62+
63+
const evt = await eventPromise
64+
65+
expect(evt.detail.id.toString()).to.equal(remotePeerId.toString())
66+
expect(evt.detail.multiaddrs).to.have.length(1)
67+
expect(evt.detail.multiaddrs[0].toString()).to.equal(
68+
`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/p2p/${remotePeerId}`
69+
)
70+
})
71+
72+
it('should not duplicate peer id in circuit relay addresses that already have one', async () => {
73+
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
74+
75+
libp2p = await createLibp2p({
76+
peerDiscovery: [() => discovery]
77+
})
78+
79+
await libp2p.start()
80+
81+
const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
82+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
83+
84+
// Address already has the target peer ID (e.g. as sent by pubsub-peer-discovery)
85+
const relayAddrWithPeerId = multiaddr(`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/p2p/${remotePeerId}`)
86+
87+
const eventPromise = pEvent<'peer:discovery', CustomEvent<PeerInfo>>(libp2p, 'peer:discovery')
88+
89+
discovery.safeDispatchEvent('peer', {
90+
detail: {
91+
id: remotePeerId,
92+
multiaddrs: [relayAddrWithPeerId]
93+
}
94+
})
95+
96+
const evt = await eventPromise
97+
98+
expect(evt.detail.id.toString()).to.equal(remotePeerId.toString())
99+
expect(evt.detail.multiaddrs).to.have.length(1)
100+
expect(evt.detail.multiaddrs[0].toString()).to.equal(
101+
`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/p2p/${remotePeerId}`
102+
)
103+
})
104+
105+
it('should not modify direct (non-relay) addresses in peer:discovery event', async () => {
106+
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
107+
108+
libp2p = await createLibp2p({
109+
peerDiscovery: [() => discovery]
110+
})
111+
112+
await libp2p.start()
113+
114+
const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
115+
const directAddr = multiaddr('/ip4/1.2.3.4/tcp/4001')
116+
117+
const eventPromise = pEvent<'peer:discovery', CustomEvent<PeerInfo>>(libp2p, 'peer:discovery')
118+
119+
discovery.safeDispatchEvent('peer', {
120+
detail: {
121+
id: remotePeerId,
122+
multiaddrs: [directAddr]
123+
}
124+
})
125+
126+
const evt = await eventPromise
127+
128+
expect(evt.detail.multiaddrs).to.have.length(1)
129+
expect(evt.detail.multiaddrs[0].toString()).to.equal('/ip4/1.2.3.4/tcp/4001')
130+
})
131+
132+
it('should append peer id to WebRTC circuit relay addresses missing one', async () => {
133+
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
134+
135+
libp2p = await createLibp2p({
136+
peerDiscovery: [() => discovery]
137+
})
138+
139+
await libp2p.start()
140+
141+
const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
142+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
143+
144+
// WebRTC relay address without target peer ID
145+
const webrtcRelayAddr = multiaddr(`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/webrtc`)
146+
147+
const eventPromise = pEvent<'peer:discovery', CustomEvent<PeerInfo>>(libp2p, 'peer:discovery')
148+
149+
discovery.safeDispatchEvent('peer', {
150+
detail: {
151+
id: remotePeerId,
152+
multiaddrs: [webrtcRelayAddr]
153+
}
154+
})
155+
156+
const evt = await eventPromise
157+
158+
expect(evt.detail.multiaddrs).to.have.length(1)
159+
expect(evt.detail.multiaddrs[0].toString()).to.equal(
160+
`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/webrtc/p2p/${remotePeerId}`
161+
)
162+
})
163+
164+
it('should not duplicate peer id in WebRTC circuit relay addresses that already have one', async () => {
165+
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
166+
167+
libp2p = await createLibp2p({
168+
peerDiscovery: [() => discovery]
169+
})
170+
171+
await libp2p.start()
172+
173+
const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
174+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
175+
176+
// WebRTC relay address that already includes the target peer ID (e.g. from pubsub-peer-discovery)
177+
const webrtcRelayAddrWithPeerId = multiaddr(`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/webrtc/p2p/${remotePeerId}`)
178+
179+
const eventPromise = pEvent<'peer:discovery', CustomEvent<PeerInfo>>(libp2p, 'peer:discovery')
180+
181+
discovery.safeDispatchEvent('peer', {
182+
detail: {
183+
id: remotePeerId,
184+
multiaddrs: [webrtcRelayAddrWithPeerId]
185+
}
186+
})
187+
188+
const evt = await eventPromise
189+
190+
expect(evt.detail.multiaddrs).to.have.length(1)
191+
expect(evt.detail.multiaddrs[0].toString()).to.equal(
192+
`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/webrtc/p2p/${remotePeerId}`
193+
)
194+
})
195+
196+
it('should handle mixed relay and direct addresses correctly in peer:discovery event', async () => {
197+
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
198+
199+
libp2p = await createLibp2p({
200+
peerDiscovery: [() => discovery]
201+
})
202+
203+
await libp2p.start()
204+
205+
const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
206+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
207+
208+
const directAddr = multiaddr('/ip4/1.2.3.4/tcp/4001')
209+
const relayAddrNoId = multiaddr(`/ip4/5.6.7.8/tcp/1234/p2p/${relayPeerId}/p2p-circuit`)
210+
const relayAddrWithId = multiaddr(`/ip4/9.10.11.12/tcp/5678/p2p/${relayPeerId}/p2p-circuit/p2p/${remotePeerId}`)
211+
212+
const eventPromise = pEvent<'peer:discovery', CustomEvent<PeerInfo>>(libp2p, 'peer:discovery')
213+
214+
discovery.safeDispatchEvent('peer', {
215+
detail: {
216+
id: remotePeerId,
217+
multiaddrs: [directAddr, relayAddrNoId, relayAddrWithId]
218+
}
219+
})
220+
221+
const evt = await eventPromise
222+
223+
const addrStrings = evt.detail.multiaddrs.map(ma => ma.toString())
224+
225+
// Direct address unchanged
226+
expect(addrStrings).to.include('/ip4/1.2.3.4/tcp/4001')
227+
// Relay without peer ID gets it appended
228+
expect(addrStrings).to.include(`/ip4/5.6.7.8/tcp/1234/p2p/${relayPeerId}/p2p-circuit/p2p/${remotePeerId}`)
229+
// Relay that already has peer ID is not modified
230+
expect(addrStrings).to.include(`/ip4/9.10.11.12/tcp/5678/p2p/${relayPeerId}/p2p-circuit/p2p/${remotePeerId}`)
231+
// No address should have a double peer ID
232+
expect(addrStrings.every(a => !a.includes(`/p2p/${remotePeerId}/p2p/${remotePeerId}`))).to.be.true()
233+
})
234+
36235
it('should ignore self on discovery', async () => {
37236
const discovery = new TypedEventEmitter<PeerDiscoveryEvents>()
38237

packages/peer-store/test/utils/dedupe-addresses.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,56 @@ describe('dedupe-addresses', () => {
6666
}])
6767
})
6868

69+
it('should preserve target peer id in circuit relay addresses', async () => {
70+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
71+
const targetPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
72+
73+
// Address that includes the target peer ID after /p2p-circuit (e.g. from pubsub-peer-discovery)
74+
const relayAddr = multiaddr(`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/p2p/${targetPeerId}`)
75+
76+
const result = await dedupeFilterAndSortAddresses(targetPeerId, async () => true, [{
77+
multiaddr: relayAddr,
78+
isCertified: false
79+
}])
80+
81+
expect(result).to.have.length(1)
82+
// The trailing /p2p/TARGET_ID must not be stripped - it is needed for dialling via relay
83+
expect(multiaddr(result[0].multiaddr).toString()).to.equal(relayAddr.toString())
84+
})
85+
86+
it('should preserve target peer id in WebRTC circuit relay addresses', async () => {
87+
const relayPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
88+
const targetPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
89+
90+
// WebRTC browser-to-browser relay address includes /webrtc before the target peer ID
91+
const webrtcRelayAddr = multiaddr(`/ip4/1.2.3.4/tcp/1234/p2p/${relayPeerId}/p2p-circuit/webrtc/p2p/${targetPeerId}`)
92+
93+
const result = await dedupeFilterAndSortAddresses(targetPeerId, async () => true, [{
94+
multiaddr: webrtcRelayAddr,
95+
isCertified: false
96+
}])
97+
98+
expect(result).to.have.length(1)
99+
expect(multiaddr(result[0].multiaddr).toString()).to.equal(webrtcRelayAddr.toString())
100+
})
101+
102+
it('should strip peer id from direct addresses', async () => {
103+
const targetPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
104+
105+
// Direct address with redundant peer ID appended (common from identify / pubsub-peer-discovery)
106+
const directAddrWithPeerId = multiaddr(`/ip4/1.2.3.4/tcp/4001/p2p/${targetPeerId}`)
107+
const directAddr = multiaddr('/ip4/1.2.3.4/tcp/4001')
108+
109+
const result = await dedupeFilterAndSortAddresses(targetPeerId, async () => true, [{
110+
multiaddr: directAddrWithPeerId,
111+
isCertified: false
112+
}])
113+
114+
expect(result).to.have.length(1)
115+
// Peer ID is stripped from direct addresses in storage (it is redundant - known from peer store key)
116+
expect(multiaddr(result[0].multiaddr).toString()).to.equal(directAddr.toString())
117+
})
118+
69119
it('should filter addresses', async () => {
70120
expect(await dedupeFilterAndSortAddresses(peerId, async () => false, [{
71121
multiaddr: addr1,

0 commit comments

Comments
 (0)