Skip to content

Commit b212490

Browse files
authored
feat(reqresp): send status messages along with reqresp responses (#11727)
1 parent f2f2634 commit b212490

File tree

7 files changed

+209
-30
lines changed

7 files changed

+209
-30
lines changed

yarn-project/p2p/src/services/reqresp/interface.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Fr } from '@aztec/foundation/fields';
33

44
import { type PeerId } from '@libp2p/interface';
55

6+
import { type ReqRespStatus } from './status.js';
7+
68
/*
79
* Request Response Sub Protocols
810
*/
@@ -31,6 +33,15 @@ export type ReqRespSubProtocolHandler = (peerId: PeerId, msg: Buffer) => Promise
3133
*/
3234
export type ReqRespSubProtocolRateLimits = Record<ReqRespSubProtocol, ProtocolRateLimitQuota>;
3335

36+
/**
37+
* The response from the ReqResp protocol
38+
* Consists of a status (Error code) and data
39+
*/
40+
export interface ReqRespResponse {
41+
status: ReqRespStatus;
42+
data: Buffer;
43+
}
44+
3445
/**
3546
* A rate limit quota
3647
*/

yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('rate limiter', () => {
7777
expect(rateLimiter.allow(ReqRespSubProtocol.TX, peerId)).toBe(false);
7878

7979
// Spy on the peer manager and check that penalizePeer is called
80-
expect(peerScoring.penalizePeer).toHaveBeenCalledWith(peerId, PeerErrorSeverity.MidToleranceError);
80+
expect(peerScoring.penalizePeer).toHaveBeenCalledWith(peerId, PeerErrorSeverity.HighToleranceError);
8181
});
8282

8383
it('Should allow requests within the global limit', () => {

yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,11 @@ export class RequestResponseRateLimiter {
200200

201201
switch (rateLimitStatus) {
202202
case RateLimitStatus.DeniedPeer:
203-
this.peerScoring.penalizePeer(peerId, PeerErrorSeverity.MidToleranceError);
203+
// Hitting a peer specific limit, we should lightly penalise the peer
204+
this.peerScoring.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError);
204205
return false;
205206
case RateLimitStatus.DeniedGlobal:
207+
// Hitting a global limit, we should not penalise the peer
206208
return false;
207209
default:
208210
return true;

yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limits.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
2525
[ReqRespSubProtocol.TX]: {
2626
peerLimit: {
2727
quotaTimeMs: 1000,
28-
quotaCount: 5,
28+
quotaCount: 10,
2929
},
3030
globalLimit: {
3131
quotaTimeMs: 1000,
32-
quotaCount: 10,
32+
quotaCount: 20,
3333
},
3434
},
3535
[ReqRespSubProtocol.BLOCK]: {

yarn-project/p2p/src/services/reqresp/reqresp.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { type PeerScoring } from '../peer-manager/peer_scoring.js';
2222
import { ReqRespSubProtocol, RequestableBuffer } from './interface.js';
2323
import { reqRespBlockHandler } from './protocols/block.js';
2424
import { GoodByeReason, reqGoodbyeHandler } from './protocols/goodbye.js';
25+
import { ReqRespStatus, prettyPrintReqRespStatus } from './status.js';
2526

2627
const PING_REQUEST = RequestableBuffer.fromBuffer(Buffer.from('ping'));
2728

@@ -126,10 +127,21 @@ describe('ReqResp', () => {
126127
await sleep(500);
127128

128129
// Default rate is set at 1 every 200 ms; so this should fire a few times
130+
const responses = [];
129131
for (let i = 0; i < 10; i++) {
130-
await nodes[0].req.sendRequestToPeer(nodes[1].p2p.peerId, ReqRespSubProtocol.PING, Buffer.from('ping'));
132+
// Response object contains the status (error flags) and data
133+
const response = await nodes[0].req.sendRequestToPeer(
134+
nodes[1].p2p.peerId,
135+
ReqRespSubProtocol.PING,
136+
Buffer.from('ping'),
137+
);
138+
responses.push(response);
131139
}
132140

141+
// Check that one of the responses gets a rate limit response
142+
const rateLimitResponse = responses.find(response => response?.status === ReqRespStatus.RATE_LIMIT_EXCEEDED);
143+
expect(rateLimitResponse).toBeDefined();
144+
133145
// Make sure the error message is logged
134146
const errorMessage = `Rate limit exceeded for ${ReqRespSubProtocol.PING} from ${nodes[0].p2p.peerId.toString()}`;
135147
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining(errorMessage));
@@ -343,7 +355,8 @@ describe('ReqResp', () => {
343355
);
344356

345357
// Expect the response to be a buffer of length 1
346-
expect(response).toEqual(Buffer.from([0x0]));
358+
expect(response?.status).toEqual(ReqRespStatus.SUCCESS);
359+
expect(response?.data).toEqual(Buffer.from([0x0]));
347360
});
348361
});
349362

@@ -413,6 +426,8 @@ describe('ReqResp', () => {
413426
const batchSize = 12;
414427
nodes = await createNodes(peerScoring, 3);
415428

429+
const requesterLoggerSpy = jest.spyOn((nodes[0].req as any).logger, 'debug');
430+
416431
await startNodes(nodes);
417432
await sleep(500);
418433
await connectToPeers(nodes);
@@ -426,6 +441,11 @@ describe('ReqResp', () => {
426441

427442
const res = await nodes[0].req.sendBatchRequest(ReqRespSubProtocol.PING, requests);
428443
expect(res).toEqual(expectResponses);
444+
445+
// Check that we did detect hitting a rate limit
446+
expect(requesterLoggerSpy).toHaveBeenCalledWith(
447+
expect.stringContaining(`${prettyPrintReqRespStatus(ReqRespStatus.RATE_LIMIT_EXCEEDED)}`),
448+
);
429449
});
430450
});
431451
});

yarn-project/p2p/src/services/reqresp/reqresp.ts

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ import { ConnectionSampler } from './connection-sampler/connection_sampler.js';
2222
import {
2323
DEFAULT_SUB_PROTOCOL_HANDLERS,
2424
DEFAULT_SUB_PROTOCOL_VALIDATORS,
25-
type ReqRespSubProtocol,
25+
type ReqRespResponse,
26+
ReqRespSubProtocol,
2627
type ReqRespSubProtocolHandlers,
2728
type ReqRespSubProtocolValidators,
2829
type SubProtocolMap,
2930
subProtocolMap,
3031
} from './interface.js';
3132
import { ReqRespMetrics } from './metrics.js';
3233
import { RequestResponseRateLimiter } from './rate-limiter/rate_limiter.js';
34+
import { ReqRespStatus, ReqRespStatusError, parseStatusChunk, prettyPrintReqRespStatus } from './status.js';
3335

3436
/**
3537
* The Request Response Service
@@ -190,10 +192,17 @@ export class ReqResp {
190192
this.logger.trace(`Sending request to peer: ${peer.toString()}`);
191193
const response = await this.sendRequestToPeer(peer, subProtocol, requestBuffer);
192194

195+
if (response && response.status !== ReqRespStatus.SUCCESS) {
196+
this.logger.debug(
197+
`Request to peer ${peer.toString()} failed with status ${prettyPrintReqRespStatus(response.status)}`,
198+
);
199+
continue;
200+
}
201+
193202
// If we get a response, return it, otherwise we iterate onto the next peer
194203
// We do not consider it a success if we have an empty buffer
195-
if (response && response.length > 0) {
196-
const object = subProtocolMap[subProtocol].response.fromBuffer(response);
204+
if (response && response.data.length > 0) {
205+
const object = subProtocolMap[subProtocol].response.fromBuffer(response.data);
197206
// The response validator handles peer punishment within
198207
const isValid = await responseValidator(request, object, peer);
199208
if (!isValid) {
@@ -311,8 +320,22 @@ export class ReqResp {
311320
for (const index of indices) {
312321
const response = await this.sendRequestToPeer(peer, subProtocol, requestBuffers[index]);
313322

314-
if (response && response.length > 0) {
315-
const object = subProtocolMap[subProtocol].response.fromBuffer(response);
323+
// Check the status of the response buffer
324+
if (response && response.status !== ReqRespStatus.SUCCESS) {
325+
this.logger.debug(
326+
`Request to peer ${peer.toString()} failed with status ${prettyPrintReqRespStatus(
327+
response.status,
328+
)}`,
329+
);
330+
331+
// If we hit a rate limit or some failure, we remove the peer and return the results,
332+
// they will be split among remaining peers and the new sampled peer
333+
batchSampler.removePeerAndReplace(peer);
334+
return { peer, results: peerResults };
335+
}
336+
337+
if (response && response.data.length > 0) {
338+
const object = subProtocolMap[subProtocol].response.fromBuffer(response.data);
316339
const isValid = await responseValidator(requests[index], object, peer);
317340

318341
if (isValid) {
@@ -394,16 +417,16 @@ export class ReqResp {
394417
peerId: PeerId,
395418
subProtocol: ReqRespSubProtocol,
396419
payload: Buffer,
397-
): Promise<Buffer | undefined> {
420+
): Promise<ReqRespResponse | undefined> {
398421
let stream: Stream | undefined;
399422
try {
400423
this.metrics.recordRequestSent(subProtocol);
401424

402425
stream = await this.connectionSampler.dialProtocol(peerId, subProtocol);
403426

404427
// Open the stream with a timeout
405-
const result = await executeTimeout<Buffer>(
406-
(): Promise<Buffer> => pipe([payload], stream!, this.readMessage.bind(this)),
428+
const result = await executeTimeout<ReqRespResponse>(
429+
(): Promise<ReqRespResponse> => pipe([payload], stream!, this.readMessage.bind(this)),
407430
this.individualRequestTimeoutMs,
408431
() => new IndividualReqRespTimeoutError(),
409432
);
@@ -447,7 +470,15 @@ export class ReqResp {
447470
* Categorize the error and log it.
448471
*/
449472
private categorizeError(e: any, peerId: PeerId, subProtocol: ReqRespSubProtocol): PeerErrorSeverity | undefined {
450-
// Non pubishable errors
473+
// Non punishable errors - we do not expect a response for goodbye messages
474+
if (subProtocol === ReqRespSubProtocol.GOODBYE) {
475+
this.logger.debug('Error encountered on goodbye sub protocol, no penalty', {
476+
peerId: peerId.toString(),
477+
subProtocol,
478+
});
479+
return undefined;
480+
}
481+
451482
// We do not punish a collective timeout, as the node triggers this interupt, independent of the peer's behaviour
452483
const logTags = {
453484
peerId: peerId.toString(),
@@ -492,14 +523,45 @@ export class ReqResp {
492523

493524
/**
494525
* Read a message returned from a stream into a single buffer
526+
*
527+
* The message is split into two components
528+
* - The first chunk should contain a control byte, indicating the status of the response see `ReqRespStatus`
529+
* - The second chunk should contain the response data
495530
*/
496-
private async readMessage(source: AsyncIterable<Uint8ArrayList>): Promise<Buffer> {
531+
private async readMessage(source: AsyncIterable<Uint8ArrayList>): Promise<ReqRespResponse> {
532+
let statusBuffer: ReqRespStatus | undefined;
497533
const chunks: Uint8Array[] = [];
498-
for await (const chunk of source) {
499-
chunks.push(chunk.subarray());
534+
535+
try {
536+
for await (const chunk of source) {
537+
if (statusBuffer === undefined) {
538+
const firstChunkBuffer = chunk.subarray();
539+
statusBuffer = parseStatusChunk(firstChunkBuffer);
540+
} else {
541+
chunks.push(chunk.subarray());
542+
}
543+
}
544+
545+
const messageData = Buffer.concat(chunks);
546+
const message: Buffer = this.snappyTransform.inboundTransformNoTopic(messageData);
547+
548+
return {
549+
status: statusBuffer ?? ReqRespStatus.UNKNOWN,
550+
data: message,
551+
};
552+
} catch (e: any) {
553+
this.logger.debug(`Reading message failed: ${e.message}`);
554+
555+
let status = ReqRespStatus.UNKNOWN;
556+
if (e instanceof ReqRespStatusError) {
557+
status = e.status;
558+
}
559+
560+
return {
561+
status,
562+
data: Buffer.from([]),
563+
};
500564
}
501-
const messageData = Buffer.concat(chunks);
502-
return this.snappyTransform.inboundTransformNoTopic(messageData);
503565
}
504566

505567
/**
@@ -525,25 +587,28 @@ export class ReqResp {
525587
private async streamHandler(protocol: ReqRespSubProtocol, { stream, connection }: IncomingStreamData) {
526588
this.metrics.recordRequestReceived(protocol);
527589

528-
// Store a reference to from this for the async generator
529-
if (!this.rateLimiter.allow(protocol, connection.remotePeer)) {
530-
this.logger.warn(`Rate limit exceeded for ${protocol} from ${connection.remotePeer}`);
590+
try {
591+
// Store a reference to from this for the async generator
592+
if (!this.rateLimiter.allow(protocol, connection.remotePeer)) {
593+
this.logger.warn(`Rate limit exceeded for ${protocol} from ${connection.remotePeer}`);
531594

532-
// TODO(#8483): handle changing peer scoring for failed rate limit, maybe differentiate between global and peer limits here when punishing
533-
await stream.close();
534-
return;
535-
}
595+
throw new ReqRespStatusError(ReqRespStatus.RATE_LIMIT_EXCEEDED);
596+
}
536597

537-
const handler = this.subProtocolHandlers[protocol];
538-
const transform = this.snappyTransform;
598+
const handler = this.subProtocolHandlers[protocol];
599+
const transform = this.snappyTransform;
539600

540-
try {
541601
await pipe(
542602
stream,
543603
async function* (source: any) {
544604
for await (const chunkList of source) {
545605
const msg = Buffer.from(chunkList.subarray());
546606
const response = await handler(connection.remotePeer, msg);
607+
608+
// Send success code first, then the response
609+
const successChunk = Buffer.from([ReqRespStatus.SUCCESS]);
610+
yield new Uint8Array(successChunk);
611+
547612
yield new Uint8Array(transform.outboundTransformNoTopic(response));
548613
}
549614
},
@@ -552,8 +617,30 @@ export class ReqResp {
552617
} catch (e: any) {
553618
this.logger.warn(e);
554619
this.metrics.recordResponseError(protocol);
620+
621+
// If we receive a known error, we use the error status in the response chunk, otherwise we categorize as unknown
622+
let errorStatus = ReqRespStatus.UNKNOWN;
623+
if (e instanceof ReqRespStatusError) {
624+
errorStatus = e.status;
625+
}
626+
627+
const sendErrorChunk = this.sendErrorChunk(errorStatus);
628+
629+
// Return and yield the response chunk
630+
await pipe(
631+
stream,
632+
async function* (_source: any) {
633+
yield* sendErrorChunk;
634+
},
635+
stream,
636+
);
555637
} finally {
556638
await stream.close();
557639
}
558640
}
641+
642+
private async *sendErrorChunk(error: ReqRespStatus): AsyncIterable<Uint8Array> {
643+
const errorChunk = Buffer.from([error]);
644+
yield new Uint8Array(errorChunk);
645+
}
559646
}

0 commit comments

Comments
 (0)