Skip to content

Commit 025c082

Browse files
authored
fix: do not overwrite addresses on identify push when none are sent (#2192)
During identify-push in response to a protocol update go-libp2p does not send a signed peer record or the current list of listen addresses. Protobuf does not let us differentiate between an empty list and no list items at all because the field is just repeated - an absence of repeated fields doesn't mean the list was omitted. When the listen address list is empty, preserve any existing addresses.
1 parent 8add94e commit 025c082

2 files changed

Lines changed: 118 additions & 18 deletions

File tree

packages/libp2p/src/identify/identify.ts

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type { Libp2pEvents, IdentifyResult, SignedPeerRecord, AbortOptions } fro
2323
import type { Connection, Stream } from '@libp2p/interface/connection'
2424
import type { TypedEventTarget } from '@libp2p/interface/events'
2525
import type { PeerId } from '@libp2p/interface/peer-id'
26-
import type { Peer, PeerStore } from '@libp2p/interface/peer-store'
26+
import type { Peer, PeerData, PeerStore } from '@libp2p/interface/peer-store'
2727
import type { Startable } from '@libp2p/interface/startable'
2828
import type { AddressManager } from '@libp2p/interface-internal/address-manager'
2929
import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
@@ -404,17 +404,30 @@ export class DefaultIdentifyService implements Startable, IdentifyService {
404404
log('received identify from %p', connection.remotePeer)
405405

406406
if (message == null) {
407-
throw new Error('Message was null or undefined')
407+
throw new CodeError('message was null or undefined', 'ERR_INVALID_MESSAGE')
408408
}
409409

410-
const peer = {
411-
addresses: message.listenAddrs.map(buf => ({
410+
const peer: PeerData = {}
411+
412+
if (message.listenAddrs.length > 0) {
413+
peer.addresses = message.listenAddrs.map(buf => ({
412414
isCertified: false,
413415
multiaddr: multiaddr(buf)
414-
})),
415-
protocols: message.protocols,
416-
metadata: new Map(),
417-
peerRecordEnvelope: message.signedPeerRecord
416+
}))
417+
}
418+
419+
if (message.protocols.length > 0) {
420+
peer.protocols = message.protocols
421+
}
422+
423+
if (message.publicKey != null) {
424+
peer.publicKey = message.publicKey
425+
426+
const peerId = await peerIdFromKeys(message.publicKey)
427+
428+
if (!peerId.equals(connection.remotePeer)) {
429+
throw new CodeError('public key did not match remote PeerId', 'ERR_INVALID_PUBLIC_KEY')
430+
}
418431
}
419432

420433
let output: SignedPeerRecord | undefined
@@ -429,12 +442,12 @@ export class DefaultIdentifyService implements Startable, IdentifyService {
429442

430443
// Verify peerId
431444
if (!peerRecord.peerId.equals(envelope.peerId)) {
432-
throw new Error('signing key does not match PeerId in the PeerRecord')
445+
throw new CodeError('signing key does not match PeerId in the PeerRecord', 'ERR_INVALID_SIGNING_KEY')
433446
}
434447

435448
// Make sure remote peer is the one sending the record
436449
if (!connection.remotePeer.equals(peerRecord.peerId)) {
437-
throw new Error('signing key does not match remote PeerId')
450+
throw new CodeError('signing key does not match remote PeerId', 'ERR_INVALID_PEER_RECORD_KEY')
438451
}
439452

440453
let existingPeer: Peer | undefined
@@ -482,15 +495,25 @@ export class DefaultIdentifyService implements Startable, IdentifyService {
482495
log('%p did not send a signed peer record', connection.remotePeer)
483496
}
484497

485-
if (message.agentVersion != null) {
486-
peer.metadata.set('AgentVersion', uint8ArrayFromString(message.agentVersion))
487-
}
498+
log('patching %p with', peer)
499+
await this.peerStore.patch(connection.remotePeer, peer)
488500

489-
if (message.protocolVersion != null) {
490-
peer.metadata.set('ProtocolVersion', uint8ArrayFromString(message.protocolVersion))
491-
}
501+
if (message.agentVersion != null || message.protocolVersion != null) {
502+
const metadata: Record<string, Uint8Array> = {}
492503

493-
await this.peerStore.patch(connection.remotePeer, peer)
504+
if (message.agentVersion != null) {
505+
metadata.AgentVersion = uint8ArrayFromString(message.agentVersion)
506+
}
507+
508+
if (message.protocolVersion != null) {
509+
metadata.ProtocolVersion = uint8ArrayFromString(message.protocolVersion)
510+
}
511+
512+
log('updating %p metadata', peer)
513+
await this.peerStore.merge(connection.remotePeer, {
514+
metadata
515+
})
516+
}
494517

495518
const result: IdentifyResult = {
496519
peerId: connection.remotePeer,

packages/libp2p/test/identify/index.spec.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { TypedEventEmitter } from '@libp2p/interface/events'
55
import { start, stop } from '@libp2p/interface/startable'
6-
import { mockConnectionGater, mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks'
6+
import { mockConnectionGater, mockRegistrar, mockUpgrader, connectionPair, mockStream } from '@libp2p/interface-compliance-tests/mocks'
77
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
88
import { PeerRecord, RecordEnvelope } from '@libp2p/peer-record'
99
import { PersistentPeerStore } from '@libp2p/peer-store'
@@ -15,6 +15,7 @@ import drain from 'it-drain'
1515
import * as lp from 'it-length-prefixed'
1616
import { pipe } from 'it-pipe'
1717
import { pbStream } from 'it-protobuf-stream'
18+
import { pushable } from 'it-pushable'
1819
import pDefer from 'p-defer'
1920
import sinon from 'sinon'
2021
import { stubInterface } from 'sinon-ts'
@@ -31,8 +32,11 @@ import { DefaultIdentifyService } from '../../src/identify/identify.js'
3132
import { identifyService, type IdentifyServiceInit, Message } from '../../src/identify/index.js'
3233
import { Identify } from '../../src/identify/pb/message.js'
3334
import { DefaultTransportManager } from '../../src/transport-manager.js'
35+
import type { Connection } from '@libp2p/interface/connection'
36+
import type { Peer } from '@libp2p/interface/peer-store'
3437
import type { IncomingStreamData } from '@libp2p/interface-internal/registrar'
3538
import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
39+
import type { Duplex, Source } from 'it-stream-types'
3640

3741
const listenMaddrs = [multiaddr('/ip4/127.0.0.1/tcp/15002/ws')]
3842

@@ -504,4 +508,77 @@ describe('identify', () => {
504508
}])
505509
expect(peer.id.publicKey).to.equalBytes(remoteComponents.peerId.publicKey)
506510
})
511+
512+
it('should not overwrite peer data when fields are omitted', async () => {
513+
const localIdentify = new DefaultIdentifyService(localComponents, defaultInit)
514+
await start(localIdentify)
515+
516+
const peer: Peer = {
517+
id: remoteComponents.peerId,
518+
addresses: [{
519+
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'),
520+
isCertified: true
521+
}],
522+
protocols: [
523+
'/proto/1'
524+
],
525+
metadata: new Map([['key', uint8ArrayFromString('value')]]),
526+
tags: new Map([['key', { value: 1 }]])
527+
}
528+
529+
await localComponents.peerStore.save(remoteComponents.peerId, peer)
530+
531+
const duplex: Duplex<any, Source<any>, any> = {
532+
source: pushable(),
533+
sink: async (source) => {
534+
await drain(source)
535+
}
536+
}
537+
538+
duplex.source.push(lp.encode.single(Identify.encode({
539+
protocols: [
540+
'/proto/2'
541+
]
542+
})))
543+
544+
await localIdentify._handlePush({
545+
stream: mockStream(duplex),
546+
connection: stubInterface<Connection>({
547+
remotePeer: remoteComponents.peerId
548+
})
549+
})
550+
551+
const updatedPeerData = await localComponents.peerStore.get(remoteComponents.peerId)
552+
expect(updatedPeerData.addresses[0].multiaddr.toString()).to.equal('/ip4/127.0.0.1/tcp/4001')
553+
expect(updatedPeerData.protocols).to.deep.equal(['/proto/2'])
554+
expect(updatedPeerData.metadata.get('key')).to.equalBytes(uint8ArrayFromString('value'))
555+
expect(updatedPeerData.tags.get('key')).to.deep.equal({ value: 1 })
556+
})
557+
558+
it('should reject incorrect public key', async () => {
559+
const localIdentify = new DefaultIdentifyService(localComponents, defaultInit)
560+
await start(localIdentify)
561+
562+
const duplex: Duplex<any, Source<any>, any> = {
563+
source: pushable(),
564+
sink: async (source) => {
565+
await drain(source)
566+
}
567+
}
568+
569+
duplex.source.push(lp.encode.single(Identify.encode({
570+
publicKey: Uint8Array.from([0, 1, 2, 3, 4])
571+
})))
572+
573+
const localPeerStorePatchSpy = sinon.spy(localComponents.peerStore, 'patch')
574+
575+
await localIdentify._handlePush({
576+
stream: mockStream(duplex),
577+
connection: stubInterface<Connection>({
578+
remotePeer: remoteComponents.peerId
579+
})
580+
})
581+
582+
expect(localPeerStorePatchSpy.called).to.be.false('patch was called when public key was invalid')
583+
})
507584
})

0 commit comments

Comments
 (0)