Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions yarn-project/p2p/src/services/dummy_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export class DummyPeerManager implements PeerManagerInterface {

export class DummyReqResp implements ReqRespInterface {
updateConfig(_config: Partial<P2PReqRespConfig>): void {}
setShouldRejectPeer(): void {}
start(
_subProtocolHandlers: ReqRespSubProtocolHandlers,
_subProtocolValidators: ReqRespSubProtocolValidators,
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/p2p/src/services/libp2p/libp2p_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,9 @@ export class LibP2PService extends WithTracer implements P2PService {
epochCache,
);

// Gate req/resp data protocols for unauthenticated peers when p2pAllowOnlyValidators is enabled
reqresp.setShouldRejectPeer(peerId => peerManager.shouldDisableP2PGossip(peerId));

// Configure application-specific scoring for gossipsub.
// The weight scales app score to align with gossipsub thresholds:
// - Disconnect (-50) × 10 = -500 = gossipThreshold (stops receiving gossip)
Expand Down
21 changes: 21 additions & 0 deletions yarn-project/p2p/src/services/reqresp/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ export type ReqRespSubProtocolValidators = {
[S in ReqRespSubProtocol]: ResponseValidator<any, any>;
};

/**
* Protocols that are always allowed without authentication, even when p2pAllowOnlyValidators is enabled.
* These are needed for the handshake and connection management flow.
* All other protocols require the remote peer to be authenticated.
*/
export const UNAUTHENTICATED_ALLOWED_PROTOCOLS: ReadonlySet<ReqRespSubProtocol> = new Set([
ReqRespSubProtocol.PING,
ReqRespSubProtocol.STATUS,
ReqRespSubProtocol.AUTH,
ReqRespSubProtocol.GOODBYE,
]);

/**
* Callback that checks whether a peer should be rejected from req/resp data protocols.
* Returns true if the peer should be rejected (i.e. p2pAllowOnlyValidators is on and peer is unauthenticated).
*/
export type ShouldRejectPeer = (peerId: string) => boolean;

export const DEFAULT_SUB_PROTOCOL_VALIDATORS: ReqRespSubProtocolValidators = {
[ReqRespSubProtocol.PING]: noopValidator,
[ReqRespSubProtocol.STATUS]: noopValidator,
Expand Down Expand Up @@ -253,5 +271,8 @@ export interface ReqRespInterface {

updateConfig(config: Partial<P2PReqRespConfig>): void;

/** Sets the callback used to reject unauthenticated peers on gated req/resp protocols. */
setShouldRejectPeer(checker: ShouldRejectPeer): void;

getConnectionSampler(): Pick<ConnectionSampler, 'getPeerListSortedByConnectionCountAsc'>;
}
100 changes: 100 additions & 0 deletions yarn-project/p2p/src/services/reqresp/reqresp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,106 @@ describe('ReqResp', () => {
});
});

describe('Authentication gating', () => {
it('should reject unauthenticated peers on all data protocols', async () => {
nodes = await createNodes(peerScoring, 2);

await startNodes(nodes);
await sleep(500);
await connectToPeers(nodes);
await sleep(500);

// Set up auth checker that rejects all peers (simulates p2pAllowOnlyValidators=true with no authenticated peers)
nodes[1].req.setShouldRejectPeer(() => true);

// All data protocols should be rejected
for (const protocol of [ReqRespSubProtocol.TX, ReqRespSubProtocol.BLOCK_TXS]) {
const resp = await nodes[0].req.sendRequestToPeer(nodes[1].p2p.peerId, protocol, Buffer.from('request'));
expect(resp.status).toEqual(ReqRespStatus.FAILURE);
}

// PING is an allowed protocol — should succeed
const pingResp = await nodes[0].req.sendRequestToPeer(nodes[1].p2p.peerId, ReqRespSubProtocol.PING, PING_REQUEST);
expectSuccess(pingResp);
expect(pingResp.data.toString('utf-8')).toEqual('pong');
});

it('should allow handshake protocols for unauthenticated peers', async () => {
nodes = await createNodes(peerScoring, 2);

await startNodes(nodes);
await sleep(500);
await connectToPeers(nodes);
await sleep(500);

// Reject all peers on gated protocols
nodes[1].req.setShouldRejectPeer(() => true);

// PING, STATUS, AUTH, GOODBYE should still work
const pingResp = await nodes[0].req.sendRequestToPeer(nodes[1].p2p.peerId, ReqRespSubProtocol.PING, PING_REQUEST);
expectSuccess(pingResp);

const statusResp = await nodes[0].req.sendRequestToPeer(
nodes[1].p2p.peerId,
ReqRespSubProtocol.STATUS,
Buffer.from('status'),
);
expectSuccess(statusResp);

const authResp = await nodes[0].req.sendRequestToPeer(
nodes[1].p2p.peerId,
ReqRespSubProtocol.AUTH,
Buffer.from('auth'),
);
expectSuccess(authResp);
});

it('should allow authenticated peers on all protocols', async () => {
nodes = await createNodes(peerScoring, 2);

await startNodes(nodes);
await sleep(500);
await connectToPeers(nodes);
await sleep(500);

// Set up auth checker that allows all peers (simulates authenticated validator)
nodes[1].req.setShouldRejectPeer(() => false);

// Data protocols should succeed for authenticated peers
const pingResp = await nodes[0].req.sendRequestToPeer(nodes[1].p2p.peerId, ReqRespSubProtocol.PING, PING_REQUEST);
expectSuccess(pingResp);
expect(pingResp.data.toString('utf-8')).toEqual('pong');

const txResp = await nodes[0].req.sendRequestToPeer(
nodes[1].p2p.peerId,
ReqRespSubProtocol.TX,
Buffer.from('request'),
);
expectSuccess(txResp);
});

it('should allow all protocols when no auth checker is set', async () => {
nodes = await createNodes(peerScoring, 2);

await startNodes(nodes);
await sleep(500);
await connectToPeers(nodes);
await sleep(500);

// No setShouldRejectPeer called — all protocols should work (backwards compatible)
const pingResp = await nodes[0].req.sendRequestToPeer(nodes[1].p2p.peerId, ReqRespSubProtocol.PING, PING_REQUEST);
expectSuccess(pingResp);
expect(pingResp.data.toString('utf-8')).toEqual('pong');

const txResp = await nodes[0].req.sendRequestToPeer(
nodes[1].p2p.peerId,
ReqRespSubProtocol.TX,
Buffer.from('request'),
);
expectSuccess(txResp);
});
});

describe('Batch requests', () => {
it('should send a batch request between many peers', async () => {
const batchSize = 9;
Expand Down
17 changes: 17 additions & 0 deletions yarn-project/p2p/src/services/reqresp/reqresp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ import {
type ReqRespSubProtocolHandlers,
type ReqRespSubProtocolRateLimits,
type ReqRespSubProtocolValidators,
type ShouldRejectPeer,
type SubProtocolMap,
UNAUTHENTICATED_ALLOWED_PROTOCOLS,
responseFromBuffer,
subProtocolSizeCalculators,
} from './interface.js';
Expand Down Expand Up @@ -72,6 +74,8 @@ export class ReqResp implements ReqRespInterface {

private snappyTransform: SnappyTransform;

private shouldRejectPeer: ShouldRejectPeer | undefined;

private metrics: ReqRespMetrics;

constructor(
Expand Down Expand Up @@ -108,6 +112,10 @@ export class ReqResp implements ReqRespInterface {
}
}

public setShouldRejectPeer(checker: ShouldRejectPeer): void {
this.shouldRejectPeer = checker;
}

get tracer() {
return this.metrics.tracer;
}
Expand Down Expand Up @@ -596,6 +604,15 @@ export class ReqResp implements ReqRespInterface {
throw new ReqRespStatusError(ReqRespStatus.RATE_LIMIT_EXCEEDED);
}

// When p2pAllowOnlyValidators is enabled, reject unauthenticated peers on data protocols
if (
!UNAUTHENTICATED_ALLOWED_PROTOCOLS.has(protocol) &&
(this.shouldRejectPeer?.(connection.remotePeer.toString()) ?? false)
) {
this.logger.debug(`Rejecting unauthenticated peer ${connection.remotePeer} on gated protocol ${protocol}`);
throw new ReqRespStatusError(ReqRespStatus.FAILURE);
}

await this.processStream(protocol, incomingStream);
} catch (err: any) {
this.metrics.recordResponseError(protocol);
Expand Down
1 change: 1 addition & 0 deletions yarn-project/p2p/src/test-helpers/mock-pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class MockReqResp implements ReqRespInterface {
}

updateConfig(_config: Partial<P2PReqRespConfig>): void {}
setShouldRejectPeer(): void {}

start(
subProtocolHandlers: Partial<ReqRespSubProtocolHandlers>,
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/p2p/src/test-helpers/reqresp-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export async function createTestLibP2PService(
epochCache,
);

reqresp.setShouldRejectPeer(peerId => peerManager.shouldDisableP2PGossip(peerId));

p2pNode.services.pubsub.score.params.appSpecificWeight = APP_SPECIFIC_WEIGHT;
p2pNode.services.pubsub.score.params.appSpecificScore = (peerId: string) =>
peerManager.shouldDisableP2PGossip(peerId) ? -Infinity : peerManager.getPeerScore(peerId);
Expand Down
Loading