Skip to content

Commit c628c44

Browse files
authored
fix: allow multiple ping messages (#2711)
Read incoming ping messges in 32 byte chunks as per the spec and echo them back to the sending peer. If the remove fails to send us 32 bytes, reset the stream with a `TimeoutError`.
1 parent 4fd7eb2 commit c628c44

File tree

3 files changed

+43
-59
lines changed

3 files changed

+43
-59
lines changed

packages/protocol-ping/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,13 @@
5454
"@libp2p/interface": "^2.0.1",
5555
"@libp2p/interface-internal": "^2.0.1",
5656
"@multiformats/multiaddr": "^12.2.3",
57-
"it-first": "^3.0.6",
58-
"it-pipe": "^3.0.1",
57+
"it-byte-stream": "^1.1.0",
5958
"uint8arrays": "^5.1.0"
6059
},
6160
"devDependencies": {
6261
"@libp2p/logger": "^5.0.1",
6362
"@libp2p/peer-id": "^5.0.1",
6463
"aegir": "^44.0.1",
65-
"it-byte-stream": "^1.0.10",
6664
"it-pair": "^2.0.6",
6765
"p-defer": "^4.0.1",
6866
"sinon-ts": "^2.0.0"

packages/protocol-ping/src/ping.ts

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { randomBytes } from '@libp2p/crypto'
2-
import { AbortError, InvalidMessageError, ProtocolError, TimeoutError } from '@libp2p/interface'
3-
import first from 'it-first'
4-
import { pipe } from 'it-pipe'
2+
import { ProtocolError, TimeoutError } from '@libp2p/interface'
3+
import { byteStream } from 'it-byte-stream'
54
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
65
import { PROTOCOL_PREFIX, PROTOCOL_NAME, PING_LENGTH, PROTOCOL_VERSION, TIMEOUT, MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS } from './constants.js'
76
import type { PingServiceComponents, PingServiceInit, PingService as PingServiceInterface } from './index.js'
@@ -60,37 +59,29 @@ export class PingService implements Startable, PingServiceInterface {
6059

6160
const { stream } = data
6261
const start = Date.now()
63-
64-
const signal = AbortSignal.timeout(this.timeout)
65-
signal.addEventListener('abort', () => {
66-
stream?.abort(new TimeoutError('ping timeout'))
62+
const bytes = byteStream(stream)
63+
64+
Promise.resolve().then(async () => {
65+
while (true) {
66+
const signal = AbortSignal.timeout(this.timeout)
67+
signal.addEventListener('abort', () => {
68+
stream?.abort(new TimeoutError('ping timeout'))
69+
})
70+
71+
const buf = await bytes.read(PING_LENGTH, {
72+
signal
73+
})
74+
await bytes.write(buf, {
75+
signal
76+
})
77+
}
6778
})
68-
69-
void pipe(
70-
stream,
71-
async function * (source) {
72-
let received = 0
73-
74-
for await (const buf of source) {
75-
received += buf.byteLength
76-
77-
if (received > PING_LENGTH) {
78-
stream?.abort(new InvalidMessageError('Too much data received'))
79-
return
80-
}
81-
82-
yield buf
83-
}
84-
},
85-
stream
86-
)
8779
.catch(err => {
8880
this.log.error('incoming ping from %p failed with error', data.connection.remotePeer, err)
8981
stream?.abort(err)
9082
})
9183
.finally(() => {
9284
const ms = Date.now() - start
93-
9485
this.log('incoming ping from %p complete in %dms', data.connection.remotePeer, ms)
9586
})
9687
}
@@ -105,7 +96,6 @@ export class PingService implements Startable, PingServiceInterface {
10596
const data = randomBytes(PING_LENGTH)
10697
const connection = await this.components.connectionManager.openConnection(peer, options)
10798
let stream: Stream | undefined
108-
let onAbort = (): void => {}
10999

110100
if (options.signal == null) {
111101
const signal = AbortSignal.timeout(this.timeout)
@@ -122,25 +112,15 @@ export class PingService implements Startable, PingServiceInterface {
122112
runOnLimitedConnection: this.runOnLimitedConnection
123113
})
124114

125-
onAbort = () => {
126-
stream?.abort(new AbortError())
127-
}
128-
129-
// make stream abortable
130-
options.signal?.addEventListener('abort', onAbort, { once: true })
115+
const bytes = byteStream(stream)
131116

132-
const result = await pipe(
133-
[data],
134-
stream,
135-
async (source) => first(source)
136-
)
117+
const [, result] = await Promise.all([
118+
bytes.write(data, options),
119+
bytes.read(PING_LENGTH, options)
120+
])
137121

138122
const ms = Date.now() - start
139123

140-
if (result == null) {
141-
throw new ProtocolError(`Did not receive a ping ack after ${ms}ms`)
142-
}
143-
144124
if (!uint8ArrayEquals(data, result.subarray())) {
145125
throw new ProtocolError(`Received wrong ping ack after ${ms}ms`)
146126
}
@@ -155,9 +135,8 @@ export class PingService implements Startable, PingServiceInterface {
155135

156136
throw err
157137
} finally {
158-
options.signal?.removeEventListener('abort', onAbort)
159138
if (stream != null) {
160-
await stream.close()
139+
await stream.close(options)
161140
}
162141
}
163142
}

packages/protocol-ping/test/index.spec.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ describe('ping', () => {
4343
logger: defaultLogger()
4444
}
4545

46-
ping = new PingService(components)
46+
ping = new PingService(components, {
47+
timeout: 50
48+
})
4749

4850
await start(ping)
4951
})
@@ -105,17 +107,20 @@ describe('ping', () => {
105107
connection: stubInterface<Connection>()
106108
})
107109

108-
const input = Uint8Array.from([0, 1, 2, 3, 4])
109-
110110
const b = byteStream(outgoingStream)
111-
void b.write(input)
112111

112+
const input = new Uint8Array(32).fill(1)
113+
void b.write(input)
113114
const output = await b.read()
114-
115115
expect(output).to.equalBytes(input)
116+
117+
const input2 = new Uint8Array(32).fill(2)
118+
void b.write(input2)
119+
const output2 = await b.read()
120+
expect(output2).to.equalBytes(input2)
116121
})
117122

118-
it('should abort stream if too much ping data received', async () => {
123+
it('should abort stream if sending stalls', async () => {
119124
const deferred = pDefer<Error>()
120125

121126
const duplex = duplexPair<any>()
@@ -135,14 +140,16 @@ describe('ping', () => {
135140
connection: stubInterface<Connection>()
136141
})
137142

138-
const input = new Uint8Array(100)
139143
const b = byteStream(outgoingStream)
140144

141-
void b.read(100)
142-
void b.write(input)
145+
// send a ping message plus a few extra bytes
146+
void b.write(new Uint8Array(35))
143147

144-
const err = await deferred.promise
148+
const pong = await b.read()
149+
expect(pong).to.have.lengthOf(32)
145150

146-
expect(err).to.have.property('name', 'InvalidMessageError')
151+
// never send the remaining 29 bytes (e.g. 64 - 35)
152+
const err = await deferred.promise
153+
expect(err).to.have.property('name', 'TimeoutError')
147154
})
148155
})

0 commit comments

Comments
 (0)