From 5f9e6bd68133b7bcc7b5c8dd293b6e976240f67e Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 12 Nov 2022 17:04:07 +0100 Subject: [PATCH 01/23] ReqResp modular for light-client --- .../network/reqresp/encoders/requestDecode.ts | 15 +- .../network/reqresp/encoders/requestEncode.ts | 12 +- .../reqresp/encoders/responseDecode.ts | 36 +- .../reqresp/encoders/responseEncode.ts | 96 ++-- .../reqresp/encodingStrategies/index.ts | 21 +- .../encodingStrategies/sszSnappy/decode.ts | 18 +- .../encodingStrategies/sszSnappy/encode.ts | 31 +- .../reqresp/handlers/beaconBlocksByRange.ts | 134 ++++-- .../reqresp/handlers/beaconBlocksByRoot.ts | 12 +- .../src/network/reqresp/handlers/index.ts | 26 +- .../reqresp/handlers/lightClientBootstrap.ts | 8 +- .../handlers/lightClientFinalityUpdate.ts | 8 +- .../handlers/lightClientOptimisticUpdate.ts | 8 +- .../handlers/lightClientUpdatesByRange.ts | 8 +- .../src/network/reqresp/interface.ts | 9 +- .../src/network/reqresp/reqResp.ts | 440 ++++++++++-------- .../src/network/reqresp/reqRespProtocol.ts | 174 +++++++ .../reqresp/request/collectResponses.ts | 16 +- .../src/network/reqresp/request/index.ts | 45 +- .../src/network/reqresp/response/index.ts | 51 +- .../network/reqresp/response/rateLimiter.ts | 57 ++- .../beacon-node/src/network/reqresp/types.ts | 274 +++-------- .../src/network/reqresp/utils/index.ts | 1 - .../src/network/reqresp/utils/protocolId.ts | 33 +- .../reqresp/utils/renderRequestBody.ts | 42 -- .../reqresp/encoders/responseTypes.test.ts | 2 +- .../request/responseTimeoutsHandler.test.ts | 4 +- .../test/unit/network/util.test.ts | 17 +- 28 files changed, 837 insertions(+), 761 deletions(-) create mode 100644 packages/beacon-node/src/network/reqresp/reqRespProtocol.ts delete mode 100644 packages/beacon-node/src/network/reqresp/utils/renderRequestBody.ts diff --git a/packages/beacon-node/src/network/reqresp/encoders/requestDecode.ts b/packages/beacon-node/src/network/reqresp/encoders/requestDecode.ts index b3645cf6fb82..c61870fc1799 100644 --- a/packages/beacon-node/src/network/reqresp/encoders/requestDecode.ts +++ b/packages/beacon-node/src/network/reqresp/encoders/requestDecode.ts @@ -1,6 +1,7 @@ import {Sink} from "it-stream-types"; import {Uint8ArrayList} from "uint8arraylist"; -import {getRequestSzzTypeByMethod, Protocol, RequestBody} from "../types.js"; +import {ForkName} from "@lodestar/params"; +import {ProtocolDefinition} from "../types.js"; import {BufferedSource} from "../utils/index.js"; import {readEncodedPayload} from "../encodingStrategies/index.js"; /** @@ -9,14 +10,14 @@ import {readEncodedPayload} from "../encodingStrategies/index.js"; * request ::= | * ``` */ -export function requestDecode( - protocol: Pick -): Sink> { +export function requestDecode( + protocol: ProtocolDefinition +): Sink> { return async function requestDecodeSink(source) { - const type = getRequestSzzTypeByMethod(protocol.method); - if (!type) { + const type = protocol.requestType(ForkName.phase0); + if (type === null) { // method has no body - return null; + return (null as unknown) as Req; } // Request has a single payload, so return immediately diff --git a/packages/beacon-node/src/network/reqresp/encoders/requestEncode.ts b/packages/beacon-node/src/network/reqresp/encoders/requestEncode.ts index 7bf93698ffcb..918b9df89907 100644 --- a/packages/beacon-node/src/network/reqresp/encoders/requestEncode.ts +++ b/packages/beacon-node/src/network/reqresp/encoders/requestEncode.ts @@ -1,4 +1,5 @@ -import {Protocol, getRequestSzzTypeByMethod, RequestBody} from "../types.js"; +import {ForkName} from "@lodestar/params"; +import {EncodedPayloadType, ProtocolDefinition} from "../types.js"; import {writeEncodedPayload} from "../encodingStrategies/index.js"; /** @@ -9,13 +10,10 @@ import {writeEncodedPayload} from "../encodingStrategies/index.js"; * Requests may contain no payload (e.g. /eth2/beacon_chain/req/metadata/1/) * if so, it would yield no byte chunks */ -export async function* requestEncode( - protocol: Pick, - requestBody: RequestBody -): AsyncGenerator { - const type = getRequestSzzTypeByMethod(protocol.method); +export async function* requestEncode(protocol: ProtocolDefinition, requestBody: Req): AsyncGenerator { + const type = protocol.requestType(ForkName.phase0); if (type && requestBody !== null) { - yield* writeEncodedPayload(requestBody, protocol.encoding, type); + yield* writeEncodedPayload({type: EncodedPayloadType.ssz, data: requestBody}, protocol.encoding, type); } } diff --git a/packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts b/packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts index 59fe006a4a5e..285008a48ad0 100644 --- a/packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts +++ b/packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts @@ -1,18 +1,11 @@ import {Uint8ArrayList} from "uint8arraylist"; import {ForkName} from "@lodestar/params"; -import {IForkDigestContext} from "@lodestar/config"; +import {Type} from "@chainsafe/ssz"; import {RespStatus} from "../../../constants/index.js"; import {BufferedSource, decodeErrorMessage} from "../utils/index.js"; import {readEncodedPayload} from "../encodingStrategies/index.js"; import {ResponseError} from "../response/index.js"; -import { - Protocol, - IncomingResponseBody, - ContextBytesType, - contextBytesTypeByProtocol, - getResponseSzzTypeByMethod, - CONTEXT_BYTES_FORK_DIGEST_LENGTH, -} from "../types.js"; +import {ContextBytesType, CONTEXT_BYTES_FORK_DIGEST_LENGTH, ContextBytesFactory, ProtocolDefinition} from "../types.js"; /** * Internal helper type to signal stream ended early @@ -29,16 +22,14 @@ enum StreamStatus { * result ::= "0" | "1" | "2" | ["128" ... "255"] * ``` */ -export function responseDecode( - forkDigestContext: IForkDigestContext, - protocol: Protocol, +export function responseDecode( + protocol: ProtocolDefinition, cbs: { onFirstHeader: () => void; onFirstResponseChunk: () => void; } -): (source: AsyncIterable) => AsyncIterable { +): (source: AsyncIterable) => AsyncIterable { return async function* responseDecodeSink(source) { - const contextBytesType = contextBytesTypeByProtocol(protocol); const bufferedSource = new BufferedSource(source as AsyncGenerator); let readFirstHeader = false; @@ -66,10 +57,10 @@ export function responseDecode( throw new ResponseError(status, errorMessage); } - const forkName = await readForkName(forkDigestContext, bufferedSource, contextBytesType); - const type = getResponseSzzTypeByMethod(protocol, forkName); + const forkName = await readContextBytes(protocol.contextBytes, bufferedSource); + const type = protocol.responseType(forkName) as Type; - yield await readEncodedPayload(bufferedSource, protocol.encoding, type); + yield await readEncodedPayload(bufferedSource, protocol.encoding, type); if (!readFirstResponseChunk) { cbs.onFirstResponseChunk(); @@ -136,18 +127,17 @@ export async function readErrorMessage(bufferedSource: BufferedSource): Promise< * While `` has a single type of `ForkDigest`, this function only parses the `ForkName` * of the `ForkDigest` or defaults to `phase0` */ -export async function readForkName( - forkDigestContext: IForkDigestContext, - bufferedSource: BufferedSource, - contextBytes: ContextBytesType +export async function readContextBytes( + contextBytes: ContextBytesFactory, + bufferedSource: BufferedSource ): Promise { - switch (contextBytes) { + switch (contextBytes.type) { case ContextBytesType.Empty: return ForkName.phase0; case ContextBytesType.ForkDigest: { const forkDigest = await readContextBytesForkDigest(bufferedSource); - return forkDigestContext.forkDigest2ForkName(forkDigest); + return contextBytes.forkDigestContext.forkDigest2ForkName(forkDigest); } } } diff --git a/packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts b/packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts index fda4fc8b8fd9..68058d4ded9b 100644 --- a/packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts +++ b/packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts @@ -1,18 +1,14 @@ import {ForkName} from "@lodestar/params"; -import {IBeaconConfig} from "@lodestar/config"; import {RespStatus, RpcResponseStatusError} from "../../../constants/index.js"; import {writeEncodedPayload} from "../encodingStrategies/index.js"; import {encodeErrorMessage} from "../utils/index.js"; import { ContextBytesType, - contextBytesTypeByProtocol, - getOutgoingSerializerByMethod, - IncomingResponseBodyByMethod, - Method, - OutgoingResponseBody, - OutgoingResponseBodyByMethod, + ContextBytesFactory, Protocol, - ResponseTypedContainer, + ProtocolDefinition, + EncodedPayload, + EncodedPayloadType, } from "../types.js"; /** @@ -24,24 +20,24 @@ import { * ``` * Note: `response` has zero or more chunks (denoted by `<>*`) */ -export function responseEncodeSuccess( - config: IBeaconConfig, - protocol: Protocol -): (source: AsyncIterable) => AsyncIterable { - const contextBytesType = contextBytesTypeByProtocol(protocol); - +export function responseEncodeSuccess( + protocol: ProtocolDefinition +): (source: AsyncIterable>) => AsyncIterable { return async function* responseEncodeSuccessTransform(source) { for await (const chunk of source) { // yield Buffer.from([RespStatus.SUCCESS]); // - from altair - const forkName = getForkNameFromResponseBody(config, protocol, chunk); - yield* writeContextBytes(config, contextBytesType, forkName); + const contextBytes = getContextBytes(protocol.contextBytes, chunk); + if (contextBytes) { + yield contextBytes as Buffer; + } // | - const serializer = getOutgoingSerializerByMethod(protocol); - yield* writeEncodedPayload(chunk, protocol.encoding, serializer); + const forkName = getForkNameFromContextBytes(protocol.contextBytes, chunk); + const respType = protocol.responseType(forkName); + yield* writeEncodedPayload(chunk, protocol.encoding, respType); } }; } @@ -74,43 +70,53 @@ export async function* responseEncodeError( * Yields byte chunks for a ``. See `ContextBytesType` for possible types. * This item is mandatory but may be empty. */ -export async function* writeContextBytes( - config: IBeaconConfig, - contextBytesType: ContextBytesType, - forkName: ForkName -): AsyncGenerator { - switch (contextBytesType) { +function getContextBytes( + contextBytes: ContextBytesFactory, + chunk: EncodedPayload +): Uint8Array | null { + switch (contextBytes.type) { // Yield nothing case ContextBytesType.Empty: - return; + return null; // Yield a fixed-width 4 byte chunk, set to the `ForkDigest` case ContextBytesType.ForkDigest: - yield config.forkName2ForkDigest(forkName) as Buffer; + switch (chunk.type) { + case EncodedPayloadType.ssz: + return contextBytes.forkDigestContext.forkName2ForkDigest( + contextBytes.forkFromResponse(chunk.data) + ) as Buffer; + + case EncodedPayloadType.bytes: + if (chunk.contextBytes.type !== ContextBytesType.ForkDigest) { + throw Error(`Expected context bytes ForkDigest but got ${chunk.contextBytes.type}`); + } + return contextBytes.forkDigestContext.forkName2ForkDigest( + contextBytes.forkDigestContext.getForkName(chunk.contextBytes.forkSlot) + ) as Buffer; + } } } -export function getForkNameFromResponseBody( - config: IBeaconConfig, - protocol: Protocol, - body: OutgoingResponseBodyByMethod[K] | IncomingResponseBodyByMethod[K] +function getForkNameFromContextBytes( + contextBytes: ContextBytesFactory, + chunk: EncodedPayload ): ForkName { - const requestTyped = {method: protocol.method, body} as ResponseTypedContainer; - - switch (requestTyped.method) { - case Method.Status: - case Method.Goodbye: - case Method.Ping: - case Method.Metadata: + switch (contextBytes.type) { + case ContextBytesType.Empty: return ForkName.phase0; - case Method.BeaconBlocksByRange: - case Method.BeaconBlocksByRoot: - return config.getForkName(requestTyped.body.slot); - case Method.LightClientBootstrap: - case Method.LightClientUpdate: - case Method.LightClientFinalityUpdate: - case Method.LightClientOptimisticUpdate: - return ForkName.altair; + // Yield a fixed-width 4 byte chunk, set to the `ForkDigest` + case ContextBytesType.ForkDigest: + switch (chunk.type) { + case EncodedPayloadType.ssz: + return contextBytes.forkFromResponse(chunk.data); + + case EncodedPayloadType.bytes: + if (chunk.contextBytes.type !== ContextBytesType.ForkDigest) { + throw Error(`Expected context bytes ForkDigest but got ${chunk.contextBytes.type}`); + } + return contextBytes.forkDigestContext.getForkName(chunk.contextBytes.forkSlot); + } } } diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/index.ts b/packages/beacon-node/src/network/reqresp/encodingStrategies/index.ts index 6874fe2f46b4..610142e633b0 100644 --- a/packages/beacon-node/src/network/reqresp/encodingStrategies/index.ts +++ b/packages/beacon-node/src/network/reqresp/encodingStrategies/index.ts @@ -1,10 +1,5 @@ -import { - Encoding, - RequestOrResponseType, - RequestOrIncomingResponseBody, - RequestOrOutgoingResponseBody, - OutgoingSerializer, -} from "../types.js"; +import {Type} from "@chainsafe/ssz"; +import {Encoding, EncodedPayload} from "../types.js"; import {BufferedSource} from "../utils/index.js"; import {readSszSnappyPayload} from "./sszSnappy/decode.js"; import {writeSszSnappyPayload} from "./sszSnappy/encode.js"; @@ -20,10 +15,10 @@ import {writeSszSnappyPayload} from "./sszSnappy/encode.js"; * | * ``` */ -export async function readEncodedPayload( +export async function readEncodedPayload( bufferedSource: BufferedSource, encoding: Encoding, - type: RequestOrResponseType + type: Type ): Promise { switch (encoding) { case Encoding.SSZ_SNAPPY: @@ -40,14 +35,14 @@ export async function readEncodedPayload | * ``` */ -export async function* writeEncodedPayload( - body: T, +export async function* writeEncodedPayload( + chunk: EncodedPayload, encoding: Encoding, - serializer: OutgoingSerializer + serializer: Type ): AsyncGenerator { switch (encoding) { case Encoding.SSZ_SNAPPY: - yield* writeSszSnappyPayload(body, serializer); + yield* writeSszSnappyPayload(chunk, serializer); break; default: diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts b/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts index 0598ad0c9d54..c765299ad526 100644 --- a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts +++ b/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts @@ -1,13 +1,13 @@ import varint from "varint"; import {Uint8ArrayList} from "uint8arraylist"; +import {Type} from "@chainsafe/ssz"; import {MAX_VARINT_BYTES} from "../../../../constants/index.js"; import {BufferedSource} from "../../utils/index.js"; -import {RequestOrResponseType, RequestOrIncomingResponseBody} from "../../types.js"; import {SnappyFramesUncompress} from "./snappyFrames/uncompress.js"; import {maxEncodedLen} from "./utils.js"; import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; -export type RequestOrResponseTypeRead = Pick; +export type TypeRead = Pick, "minSize" | "maxSize" | "deserialize">; /** * ssz_snappy encoding strategy reader. @@ -16,10 +16,7 @@ export type RequestOrResponseTypeRead = Pick | * ``` */ -export async function readSszSnappyPayload( - bufferedSource: BufferedSource, - type: RequestOrResponseTypeRead -): Promise { +export async function readSszSnappyPayload(bufferedSource: BufferedSource, type: TypeRead): Promise { const sszDataLength = await readSszSnappyHeader(bufferedSource, type); const bytes = await readSszSnappyBody(bufferedSource, sszDataLength); @@ -32,7 +29,7 @@ export async function readSszSnappyPayload + type: Pick, "minSize" | "maxSize"> ): Promise { for await (const buffer of bufferedSource) { // Get next bytes if empty @@ -129,12 +126,9 @@ export async function readSszSnappyBody(bufferedSource: BufferedSource, sszDataL * Deseralizes SSZ body. * `isSszTree` option allows the SignedBeaconBlock type to be deserialized as a tree */ -function deserializeSszBody( - bytes: Uint8Array, - type: RequestOrResponseTypeRead -): T { +function deserializeSszBody(bytes: Uint8Array, type: TypeRead): T { try { - return type.deserialize(bytes) as T; + return type.deserialize(bytes); } catch (e) { throw new SszSnappyError({code: SszSnappyErrorCode.DESERIALIZE_ERROR, deserializeError: e as Error}); } diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/encode.ts b/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/encode.ts index 1231086572e7..883c01e29e20 100644 --- a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/encode.ts +++ b/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/encode.ts @@ -1,7 +1,8 @@ import varint from "varint"; import {source} from "stream-to-it"; +import {Type} from "@chainsafe/ssz"; import snappy from "@chainsafe/snappy-stream"; -import {RequestOrOutgoingResponseBody, OutgoingSerializer} from "../../types.js"; +import {EncodedPayload, EncodedPayloadType} from "../../types.js"; import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; /** @@ -11,11 +12,8 @@ import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; * | * ``` */ -export async function* writeSszSnappyPayload( - body: T, - serializer: OutgoingSerializer -): AsyncGenerator { - const serializedBody = serializeSszBody(body, serializer); +export async function* writeSszSnappyPayload(body: EncodedPayload, type: Type): AsyncGenerator { + const serializedBody = serializeSszBody(body, type); yield* encodeSszSnappy(serializedBody); } @@ -47,12 +45,19 @@ export async function* encodeSszSnappy(bytes: Buffer): AsyncGenerator { /** * Returns SSZ serialized body. Wrapps errors with SszSnappyError.SERIALIZE_ERROR */ -function serializeSszBody(body: T, serializer: OutgoingSerializer): Buffer { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bytes = serializer.serialize(body as any); - return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length); - } catch (e) { - throw new SszSnappyError({code: SszSnappyErrorCode.SERIALIZE_ERROR, serializeError: e as Error}); +function serializeSszBody(chunk: EncodedPayload, type: Type): Buffer { + switch (chunk.type) { + case EncodedPayloadType.bytes: + return chunk.bytes as Buffer; + + case EncodedPayloadType.ssz: { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bytes = type.serialize(chunk.data); + return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length); + } catch (e) { + throw new SszSnappyError({code: SszSnappyErrorCode.SERIALIZE_ERROR, serializeError: e as Error}); + } + } } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts index 82ca6f0fd67c..05b201d5142f 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -1,62 +1,42 @@ import {GENESIS_SLOT, MAX_REQUEST_BLOCKS} from "@lodestar/params"; -import {phase0, Slot} from "@lodestar/types"; +import {allForks, phase0, Slot} from "@lodestar/types"; import {fromHexString} from "@chainsafe/ssz"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; import {RespStatus} from "../../../constants/index.js"; import {ResponseError} from "../response/index.js"; -import {ReqRespBlockResponse} from "../types.js"; +import {ContextBytesType, EncodedPayload, EncodedPayloadType} from "../types.js"; // TODO: Unit test export async function* onBeaconBlocksByRange( - requestBody: phase0.BeaconBlocksByRangeRequest, + request: phase0.BeaconBlocksByRangeRequest, chain: IBeaconChain, db: IBeaconDb -): AsyncIterable { - const {startSlot, step} = requestBody; - let {count} = requestBody; - if (step < 1) { - throw new ResponseError(RespStatus.INVALID_REQUEST, "step < 1"); - } - if (count < 1) { - throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1"); - } - // TODO: validate against MIN_EPOCHS_FOR_BLOCK_REQUESTS - if (startSlot < GENESIS_SLOT) { - throw new ResponseError(RespStatus.INVALID_REQUEST, "startSlot < genesis"); - } - - if (step > 1) { - // step > 1 is deprecated, see https://github.com/ethereum/consensus-specs/pull/2856 - count = 1; - } - - if (count > MAX_REQUEST_BLOCKS) { - count = MAX_REQUEST_BLOCKS; - } - +): AsyncIterable> { + const {startSlot, count} = validateBeaconBlocksByRangeRequest(request); const lt = startSlot + count; // step < 1 was validated above const archivedBlocksStream = getFinalizedBlocksByRange(startSlot, lt, db); - yield* injectRecentBlocks(archivedBlocksStream, chain, db, requestBody); -} + // Inject recent blocks, not in the finalized cold DB -export async function* injectRecentBlocks( - archiveStream: AsyncIterable, - chain: IBeaconChain, - db: IBeaconDb, - request: phase0.BeaconBlocksByRangeRequest -): AsyncGenerator { let totalBlock = 0; let slot = -1; - for await (const p2pBlock of archiveStream) { + for await (const block of archivedBlocksStream) { totalBlock++; - yield p2pBlock; - slot = p2pBlock.slot; + slot = block.slot; + yield { + type: EncodedPayloadType.bytes, + bytes: block.bytes, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.slot, + }, + }; } + slot = slot === -1 ? request.startSlot : slot + request.step; const upperSlot = request.startSlot + request.count * request.step; const slots = [] as number[]; @@ -65,11 +45,18 @@ export async function* injectRecentBlocks( slot += request.step; } - const p2pBlocks = await getUnfinalizedBlocksAtSlots(slots, {chain, db}); - for (const p2pBlock of p2pBlocks) { - if (p2pBlock !== undefined) { + const unfinalizedBlocks = await getUnfinalizedBlocksAtSlots(slots, {chain, db}); + for (const block of unfinalizedBlocks) { + if (block !== undefined) { totalBlock++; - yield p2pBlock; + yield { + type: EncodedPayloadType.bytes, + bytes: block.bytes, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.slot, + }, + }; } } if (totalBlock === 0) { @@ -77,14 +64,20 @@ export async function* injectRecentBlocks( } } -async function* getFinalizedBlocksByRange(gte: number, lt: number, db: IBeaconDb): AsyncIterable { +async function* getFinalizedBlocksByRange( + gte: number, + lt: number, + db: IBeaconDb +): AsyncIterable<{slot: Slot; bytes: Uint8Array}> { const binaryEntriesStream = db.blockArchive.binaryEntriesStream({ gte, lt, }); for await (const {key, value} of binaryEntriesStream) { - const slot = db.blockArchive.decodeKey(key); - yield {bytes: value, slot}; + yield { + slot: db.blockArchive.decodeKey(key), + bytes: value, + }; } } @@ -92,7 +85,7 @@ async function* getFinalizedBlocksByRange(gte: number, lt: number, db: IBeaconDb async function getUnfinalizedBlocksAtSlots( slots: Slot[], {chain, db}: {chain: IBeaconChain; db: IBeaconDb} -): Promise { +): Promise<{slot: Slot; bytes: Uint8Array}[]> { if (slots.length === 0) { return []; } @@ -110,8 +103,51 @@ async function getUnfinalizedBlocksAtSlots( } } - const unfinalizedBlocks = await Promise.all(slots.map((slot) => blockRootsPerSlot.get(slot))); - return unfinalizedBlocks - .map((block, i) => ({bytes: block, slot: slots[i]})) - .filter((p2pBlock): p2pBlock is ReqRespBlockResponse => p2pBlock.bytes != null); + const unfinalizedBlocksOrNull = await Promise.all(slots.map((slot) => blockRootsPerSlot.get(slot))); + + const unfinalizedBlocks: {slot: Slot; bytes: Uint8Array}[] = []; + + for (let i = 0; i < unfinalizedBlocksOrNull.length; i++) { + const block = unfinalizedBlocksOrNull[i]; + if (block) { + unfinalizedBlocks.push({ + slot: slots[i], + bytes: block, + }); + } + } + + return unfinalizedBlocks; +} + +function validateBeaconBlocksByRangeRequest( + request: phase0.BeaconBlocksByRangeRequest +): phase0.BeaconBlocksByRangeRequest { + const {startSlot, step} = request; + let {count} = request; + if (step < 1) { + throw new ResponseError(RespStatus.INVALID_REQUEST, "step < 1"); + } + if (count < 1) { + throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1"); + } + // TODO: validate against MIN_EPOCHS_FOR_BLOCK_REQUESTS + if (startSlot < GENESIS_SLOT) { + throw new ResponseError(RespStatus.INVALID_REQUEST, "startSlot < genesis"); + } + + if (step > 1) { + // step > 1 is deprecated, see https://github.com/ethereum/consensus-specs/pull/2856 + count = 1; + } + + if (count > MAX_REQUEST_BLOCKS) { + count = MAX_REQUEST_BLOCKS; + } + + return { + startSlot, + step, + count, + }; } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts index a08881258983..26273d56440a 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -1,14 +1,14 @@ -import {phase0, Slot} from "@lodestar/types"; +import {allForks, phase0, Slot} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; import {getSlotFromBytes} from "../../../util/multifork.js"; -import {ReqRespBlockResponse} from "../types.js"; +import {ContextBytesType, EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onBeaconBlocksByRoot( requestBody: phase0.BeaconBlocksByRootRequest, chain: IBeaconChain, db: IBeaconDb -): AsyncIterable { +): AsyncIterable> { for (const blockRoot of requestBody) { const root = blockRoot; const summary = chain.forkChoice.getBlock(root); @@ -29,8 +29,12 @@ export async function* onBeaconBlocksByRoot( } if (blockBytes) { yield { + type: EncodedPayloadType.bytes, bytes: blockBytes, - slot: slot ?? getSlotFromBytes(blockBytes), + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: slot ?? getSlotFromBytes(blockBytes), + }, }; } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 5139bdd43523..123a894c750b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -1,7 +1,7 @@ -import {altair, phase0, Root} from "@lodestar/types"; +import {allForks, altair, phase0, Root} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; -import {ReqRespBlockResponse} from "../types.js"; +import {EncodedPayload, EncodedPayloadType} from "../types.js"; import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js"; import {onBeaconBlocksByRoot} from "./beaconBlocksByRoot.js"; import {onLightClientBootstrap} from "./lightClientBootstrap.js"; @@ -10,13 +10,19 @@ import {onLightClientFinalityUpdate} from "./lightClientFinalityUpdate.js"; import {onLightClientOptimisticUpdate} from "./lightClientOptimisticUpdate.js"; export type ReqRespHandlers = { - onStatus(): AsyncIterable; - onBeaconBlocksByRange(req: phase0.BeaconBlocksByRangeRequest): AsyncIterable; - onBeaconBlocksByRoot(req: phase0.BeaconBlocksByRootRequest): AsyncIterable; - onLightClientBootstrap(req: Root): AsyncIterable; - onLightClientUpdatesByRange(req: altair.LightClientUpdatesByRange): AsyncIterable; - onLightClientFinalityUpdate(): AsyncIterable; - onLightClientOptimisticUpdate(): AsyncIterable; + onStatus(): AsyncIterable>; + onBeaconBlocksByRange( + req: phase0.BeaconBlocksByRangeRequest + ): AsyncIterable>; + onBeaconBlocksByRoot( + req: phase0.BeaconBlocksByRootRequest + ): AsyncIterable>; + onLightClientBootstrap(req: Root): AsyncIterable>; + onLightClientUpdatesByRange( + req: altair.LightClientUpdatesByRange + ): AsyncIterable>; + onLightClientFinalityUpdate(): AsyncIterable>; + onLightClientOptimisticUpdate(): AsyncIterable>; }; /** @@ -26,7 +32,7 @@ export type ReqRespHandlers = { export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconChain}): ReqRespHandlers { return { async *onStatus() { - yield chain.getStatus(); + yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; }, async *onBeaconBlocksByRange(req) { yield* onBeaconBlocksByRange(req, chain, db); diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts index f1555aa5561b..45bd9ed7fca3 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts @@ -3,13 +3,17 @@ import {IBeaconChain} from "../../../chain/index.js"; import {ResponseError} from "../response/index.js"; import {RespStatus} from "../../../constants/index.js"; import {LightClientServerError, LightClientServerErrorCode} from "../../../chain/errors/lightClientError.js"; +import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientBootstrap( requestBody: Root, chain: IBeaconChain -): AsyncIterable { +): AsyncIterable> { try { - yield await chain.lightClientServer.getBootstrap(requestBody); + yield { + type: EncodedPayloadType.ssz, + data: await chain.lightClientServer.getBootstrap(requestBody), + }; } catch (e) { if ((e as LightClientServerError).type?.code === LightClientServerErrorCode.RESOURCE_UNAVAILABLE) { throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, (e as Error).message); diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts index f6dc16aab754..fdc0a29af653 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts @@ -2,14 +2,18 @@ import {altair} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; import {ResponseError} from "../response/index.js"; import {RespStatus} from "../../../constants/index.js"; +import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientFinalityUpdate( chain: IBeaconChain -): AsyncIterable { +): AsyncIterable> { const finalityUpdate = chain.lightClientServer.getFinalityUpdate(); if (finalityUpdate === null) { throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest finality update available"); } else { - yield finalityUpdate; + yield { + type: EncodedPayloadType.ssz, + data: finalityUpdate, + }; } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts index 4a5dcf1be016..b7fcf0d285ad 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts @@ -2,14 +2,18 @@ import {altair} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; import {ResponseError} from "../response/index.js"; import {RespStatus} from "../../../constants/index.js"; +import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientOptimisticUpdate( chain: IBeaconChain -): AsyncIterable { +): AsyncIterable> { const optimisticUpdate = chain.lightClientServer.getOptimisticUpdate(); if (optimisticUpdate === null) { throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest optimistic update available"); } else { - yield optimisticUpdate; + yield { + type: EncodedPayloadType.ssz, + data: optimisticUpdate, + }; } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts index 884a196da8a6..d2b43af9df37 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts @@ -4,15 +4,19 @@ import {IBeaconChain} from "../../../chain/index.js"; import {LightClientServerError, LightClientServerErrorCode} from "../../../chain/errors/lightClientError.js"; import {ResponseError} from "../response/errors.js"; import {RespStatus} from "../../../constants/network.js"; +import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientUpdatesByRange( requestBody: altair.LightClientUpdatesByRange, chain: IBeaconChain -): AsyncIterable { +): AsyncIterable> { const count = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, requestBody.count); for (let period = requestBody.startPeriod; period < requestBody.startPeriod + count; period++) { try { - yield await chain.lightClientServer.getUpdate(period); + yield { + type: EncodedPayloadType.ssz, + data: await chain.lightClientServer.getUpdate(period), + }; } catch (e) { if ((e as LightClientServerError).type?.code === LightClientServerErrorCode.RESOURCE_UNAVAILABLE) { throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, (e as Error).message); diff --git a/packages/beacon-node/src/network/reqresp/interface.ts b/packages/beacon-node/src/network/reqresp/interface.ts index 296591775587..b8d4d2138885 100644 --- a/packages/beacon-node/src/network/reqresp/interface.ts +++ b/packages/beacon-node/src/network/reqresp/interface.ts @@ -10,7 +10,6 @@ import {INetworkEventBus} from "../events.js"; import {PeersData} from "../peers/peersData.js"; import {IMetrics} from "../../metrics/index.js"; import {ReqRespHandlers} from "./handlers/index.js"; -import {RequestTypedContainer} from "./types.js"; export interface IReqResp { start(): void; @@ -47,10 +46,10 @@ export interface IReqRespModules { * Rate limiter interface for inbound and outbound requests. */ export interface IRateLimiter { - /** - * Allow to request or response based on rate limit params configured. - */ - allowRequest(peerId: PeerId, requestTyped: RequestTypedContainer): boolean; + /** Allow to request or response based on rate limit params configured. */ + allowRequest(peerId: PeerId): boolean; + /** Rate limit check for block count */ + allowBlockByRequest(peerId: PeerId, numBlock: number): boolean; /** * Prune by peer id diff --git a/packages/beacon-node/src/network/reqresp/reqResp.ts b/packages/beacon-node/src/network/reqresp/reqResp.ts index ed89583151ef..01927e64c21f 100644 --- a/packages/beacon-node/src/network/reqresp/reqResp.ts +++ b/packages/beacon-node/src/network/reqresp/reqResp.ts @@ -1,120 +1,256 @@ -import {setMaxListeners} from "node:events"; -import {Libp2p} from "libp2p"; import {PeerId} from "@libp2p/interface-peer-id"; -import {Connection, Stream} from "@libp2p/interface-connection"; +import {Type} from "@chainsafe/ssz"; import {ForkName} from "@lodestar/params"; -import {IBeaconConfig} from "@lodestar/config"; -import {allForks, altair, phase0} from "@lodestar/types"; -import {ILogger} from "@lodestar/utils"; +import {allForks, altair, phase0, Root, Slot, ssz} from "@lodestar/types"; +import {toHex} from "@lodestar/utils"; import {RespStatus, timeoutOptions} from "../../constants/index.js"; -import {PeersData} from "../peers/peersData.js"; import {MetadataController} from "../metadata.js"; import {IPeerRpcScoreStore} from "../peers/score.js"; import {INetworkEventBus, NetworkEvent} from "../events.js"; -import {IMetrics} from "../../metrics/metrics.js"; import {IReqResp, IReqRespModules, IRateLimiter} from "./interface.js"; -import {sendRequest} from "./request/index.js"; -import {handleRequest, ResponseError} from "./response/index.js"; -import {onOutgoingReqRespError} from "./score.js"; -import {assertSequentialBlocksInRange, formatProtocolId} from "./utils/index.js"; +import {ResponseError} from "./response/index.js"; +import {assertSequentialBlocksInRange} from "./utils/index.js"; import {ReqRespHandlers} from "./handlers/index.js"; -import {RequestError, RequestErrorCode} from "./request/index.js"; import { Method, Version, Encoding, - Protocol, - OutgoingResponseBody, - RequestBody, + ContextBytesType, + ContextBytesFactory, + EncodedPayload, + EncodedPayloadType, RequestTypedContainer, - protocolsSupported, - IncomingResponseBody, } from "./types.js"; import {InboundRateLimiter, RateLimiterOpts} from "./response/rateLimiter.js"; +import {ReqRespProtocol} from "./reqRespProtocol.js"; +import {RequestError} from "./request/errors.js"; +import {onOutgoingReqRespError} from "./score.js"; export type IReqRespOptions = Partial; +/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ +export type ReqRespBlockResponse = { + /** Deserialized data of allForks.SignedBeaconBlock */ + bytes: Uint8Array; + slot: Slot; +}; + /** * Implementation of Ethereum Consensus p2p Req/Resp domain. * For the spec that this code is based on, see: * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ -export class ReqResp implements IReqResp { - private config: IBeaconConfig; - private libp2p: Libp2p; - private readonly peersData: PeersData; - private logger: ILogger; +export class ReqResp extends ReqRespProtocol implements IReqResp { private reqRespHandlers: ReqRespHandlers; private metadataController: MetadataController; private peerRpcScores: IPeerRpcScoreStore; private inboundRateLimiter: IRateLimiter; private networkEventBus: INetworkEventBus; - private controller = new AbortController(); - private options?: IReqRespOptions; - private reqCount = 0; - private respCount = 0; - private metrics: IMetrics | null; constructor(modules: IReqRespModules, options: IReqRespOptions & RateLimiterOpts) { - this.config = modules.config; - this.libp2p = modules.libp2p; - this.peersData = modules.peersData; - this.logger = modules.logger; + super(modules, options); + + const {reqRespHandlers, config} = modules; + + // Single chunk protocols + + this.registerProtocol({ + method: Method.Status, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: this.onStatus.bind(this), + requestType: () => ssz.phase0.Status, + responseType: () => ssz.phase0.Status, + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }); + + this.registerProtocol({ + method: Method.Goodbye, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: this.onGoodbye.bind(this), + requestType: () => ssz.phase0.Goodbye, + responseType: () => ssz.phase0.Goodbye, + renderRequestBody: (req) => req.toString(10), + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }); + + this.registerProtocol({ + method: Method.Ping, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: this.onPing.bind(this), + requestType: () => ssz.phase0.Ping, + responseType: () => ssz.phase0.Ping, + renderRequestBody: (req) => req.toString(10), + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }); + + // V1 -> phase0.Metadata, V2 -> altair.Metadata + for (const [version, responseType] of [ + [Version.V1, ssz.phase0.Metadata], + [Version.V2, ssz.altair.Metadata], + ] as [Version, Type][]) { + this.registerProtocol({ + method: Method.Metadata, + version, + encoding: Encoding.SSZ_SNAPPY, + handler: this.onMetadata.bind(this), + requestType: () => null, + responseType: () => responseType, + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }); + } + + // Block by protocols + + const contextBytesEmpty = {type: ContextBytesType.Empty}; + + const contextBytesBlocksByV2: ContextBytesFactory = { + type: ContextBytesType.ForkDigest, + forkDigestContext: config, + forkFromResponse: (block) => config.getForkName(block.message.slot), + }; + + for (const [version, contextBytes] of [ + [Version.V1, contextBytesEmpty], + [Version.V2, contextBytesBlocksByV2], + ] as [Version, ContextBytesFactory][]) { + this.registerProtocol({ + method: Method.BeaconBlocksByRange, + version, + encoding: Encoding.SSZ_SNAPPY, + handler: this.onBeaconBlocksByRange.bind(this), + requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, + contextBytes, + isSingleResponse: false, + }); + + this.registerProtocol({ + method: Method.BeaconBlocksByRoot, + version, + encoding: Encoding.SSZ_SNAPPY, + handler: this.onBeaconBlocksByRoot.bind(this), + requestType: () => ssz.phase0.BeaconBlocksByRootRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), + contextBytes, + isSingleResponse: false, + }); + } + + // Lightclient methods + + function getContextBytesLightclient(forkFromResponse: (response: T) => ForkName): ContextBytesFactory { + return { + type: ContextBytesType.ForkDigest, + forkDigestContext: config, + forkFromResponse, + }; + } + + this.registerProtocol({ + method: Method.LightClientBootstrap, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: reqRespHandlers.onLightClientBootstrap, + requestType: () => ssz.Root, + responseType: () => ssz.altair.LightClientBootstrap, + renderRequestBody: (req) => toHex(req), + contextBytes: getContextBytesLightclient((bootstrap) => config.getForkName(bootstrap.header.slot)), + isSingleResponse: true, + }); + + this.registerProtocol({ + method: Method.LightClientUpdatesByRange, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: reqRespHandlers.onLightClientUpdatesByRange, + requestType: () => ssz.altair.LightClientUpdatesByRange, + responseType: () => ssz.altair.LightClientUpdate, + renderRequestBody: (req) => `${req.startPeriod},${req.count}`, + contextBytes: getContextBytesLightclient((update) => config.getForkName(update.signatureSlot)), + isSingleResponse: false, + }); + + this.registerProtocol({ + method: Method.LightClientFinalityUpdate, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: reqRespHandlers.onLightClientFinalityUpdate, + requestType: () => null, + responseType: () => ssz.altair.LightClientFinalityUpdate, + contextBytes: getContextBytesLightclient((update) => config.getForkName(update.signatureSlot)), + isSingleResponse: true, + }); + + this.registerProtocol({ + method: Method.LightClientOptimisticUpdate, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: reqRespHandlers.onLightClientOptimisticUpdate, + requestType: () => null, + responseType: () => ssz.altair.LightClientOptimisticUpdate, + contextBytes: getContextBytesLightclient((update) => config.getForkName(update.signatureSlot)), + isSingleResponse: true, + }); + this.reqRespHandlers = modules.reqRespHandlers; this.metadataController = modules.metadata; this.peerRpcScores = modules.peerRpcScores; this.inboundRateLimiter = new InboundRateLimiter(options, {...modules}); this.networkEventBus = modules.networkEventBus; - this.options = options; - this.metrics = modules.metrics; } async start(): Promise { - this.controller = new AbortController(); - // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 - // Since it is perfectly fine to have listeners > 10 - setMaxListeners(Infinity, this.controller.signal); - for (const [method, version, encoding] of protocolsSupported) { - await this.libp2p.handle( - formatProtocolId(method, version, encoding), - this.getRequestHandler({method, version, encoding}) - ); - } + await super.start(); this.inboundRateLimiter.start(); } async stop(): Promise { - for (const [method, version, encoding] of protocolsSupported) { - await this.libp2p.unhandle(formatProtocolId(method, version, encoding)); - } - this.controller.abort(); + await super.stop(); this.inboundRateLimiter.stop(); } + pruneOnPeerDisconnect(peerId: PeerId): void { + this.inboundRateLimiter.prune(peerId); + } + async status(peerId: PeerId, request: phase0.Status): Promise { - return await this.sendRequest(peerId, Method.Status, [Version.V1], request); + return await this.sendRequest(peerId, Method.Status, [Version.V1], request); } async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { - await this.sendRequest(peerId, Method.Goodbye, [Version.V1], request); + await this.sendRequest(peerId, Method.Goodbye, [Version.V1], request); } async ping(peerId: PeerId): Promise { - return await this.sendRequest(peerId, Method.Ping, [Version.V1], this.metadataController.seqNumber); + return await this.sendRequest( + peerId, + Method.Ping, + [Version.V1], + this.metadataController.seqNumber + ); } async metadata(peerId: PeerId, fork?: ForkName): Promise { // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; - return await this.sendRequest(peerId, Method.Metadata, versions, null); + return await this.sendRequest(peerId, Method.Metadata, versions, null); } async beaconBlocksByRange( peerId: PeerId, request: phase0.BeaconBlocksByRangeRequest ): Promise { - const blocks = await this.sendRequest( + const blocks = await this.sendRequest( peerId, Method.BeaconBlocksByRange, [Version.V2, Version.V1], // Prioritize V2 @@ -129,7 +265,7 @@ export class ReqResp implements IReqResp { peerId: PeerId, request: phase0.BeaconBlocksByRootRequest ): Promise { - return await this.sendRequest( + return await this.sendRequest( peerId, Method.BeaconBlocksByRoot, [Version.V2, Version.V1], // Prioritize V2 @@ -138,12 +274,8 @@ export class ReqResp implements IReqResp { ); } - pruneOnPeerDisconnect(peerId: PeerId): void { - this.inboundRateLimiter.prune(peerId); - } - - async lightClientBootstrap(peerId: PeerId, request: Uint8Array): Promise { - return await this.sendRequest( + async lightClientBootstrap(peerId: PeerId, request: Root): Promise { + return await this.sendRequest( peerId, Method.LightClientBootstrap, [Version.V1], @@ -152,7 +284,7 @@ export class ReqResp implements IReqResp { } async lightClientOptimisticUpdate(peerId: PeerId): Promise { - return await this.sendRequest( + return await this.sendRequest( peerId, Method.LightClientOptimisticUpdate, [Version.V1], @@ -161,7 +293,7 @@ export class ReqResp implements IReqResp { } async lightClientFinalityUpdate(peerId: PeerId): Promise { - return await this.sendRequest( + return await this.sendRequest( peerId, Method.LightClientFinalityUpdate, [Version.V1], @@ -173,150 +305,78 @@ export class ReqResp implements IReqResp { peerId: PeerId, request: altair.LightClientUpdatesByRange ): Promise { - return await this.sendRequest( + return await this.sendRequest( peerId, - Method.LightClientUpdate, + Method.LightClientUpdatesByRange, [Version.V1], request, request.count ); } - // Helper to reduce code duplication - private async sendRequest( - peerId: PeerId, - method: Method, - versions: Version[], - body: RequestBody, - maxResponses = 1 - ): Promise { - this.metrics?.reqResp.outgoingRequests.inc({method}); - const timer = this.metrics?.reqResp.outgoingRequestRoundtripTime.startTimer({method}); - - try { - const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; - const result = await sendRequest( - {forkDigestContext: this.config, logger: this.logger, libp2p: this.libp2p, peersData: this.peersData}, - peerId, - method, - encoding, - versions, - body, - maxResponses, - this.controller.signal, - this.options, - this.reqCount++ - ); - - return result; - } catch (e) { - this.metrics?.reqResp.outgoingErrors.inc({method}); - - if (e instanceof RequestError) { - if (e.type.code === RequestErrorCode.DIAL_ERROR || e.type.code === RequestErrorCode.DIAL_TIMEOUT) { - this.metrics?.reqResp.dialErrors.inc(); - } - const peerAction = onOutgoingReqRespError(e, method); - if (peerAction !== null) { - this.peerRpcScores.applyAction(peerId, peerAction, e.type.code); - } - } - - throw e; - } finally { - timer?.(); + /** + * @override Rate limit requests before decoding request body + */ + protected onIncomingRequest(peerId: PeerId, method: Method): void { + if (method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); } } - private getRequestHandler({method, version, encoding}: Protocol) { - return async ({connection, stream}: {connection: Connection; stream: Stream}) => { - const peerId = connection.remotePeer; - - // TODO: Do we really need this now that there is only one encoding? - // Remember the prefered encoding of this peer - if (method === Method.Status) { - this.peersData.setEncodingPreference(peerId.toString(), encoding); - } - - this.metrics?.reqResp.incomingRequests.inc({method}); - const timer = this.metrics?.reqResp.incomingRequestHandlerTime.startTimer({method}); - - try { - await handleRequest( - {config: this.config, logger: this.logger, peersData: this.peersData}, - this.onRequest.bind(this), - stream, - peerId, - {method, version, encoding}, - this.controller.signal, - this.respCount++ - ); - // TODO: Do success peer scoring here - } catch { - this.metrics?.reqResp.incomingErrors.inc({method}); - - // TODO: Do error peer scoring here - // Must not throw since this is an event handler - } finally { - timer?.(); - } - }; + protected onOutgoingReqRespError(peerId: PeerId, method: Method, error: RequestError): void { + const peerAction = onOutgoingReqRespError(error, method); + if (peerAction !== null) { + this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); + } } - private async *onRequest( - protocol: Protocol, - requestBody: RequestBody, - peerId: PeerId - ): AsyncIterable { - const requestTyped = {method: protocol.method, body: requestBody} as RequestTypedContainer; + private onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { + // Allow onRequest to return and close the stream + // For Goodbye there may be a race condition where the listener of `receivedGoodbye` + // disconnects in the same syncronous call, preventing the stream from ending cleanly + setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); + } + + private async *onStatus(req: phase0.Status, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Status, body: req}, peerId); + yield* this.reqRespHandlers.onStatus(); + } + + private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); + yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; + } - if (requestTyped.method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId, requestTyped)) { + private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); + yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; + } + + private async *onMetadata(req: null, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); + + // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property + // It's safe to return altair.Metadata here for all versions + yield {type: EncodedPayloadType.ssz, data: this.metadataController.json}; + } + + private async *onBeaconBlocksByRange( + req: phase0.BeaconBlocksByRangeRequest, + peerId: PeerId + ): AsyncIterable> { + if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); } + yield* this.reqRespHandlers.onBeaconBlocksByRange(req); + } - switch (requestTyped.method) { - case Method.Ping: - yield this.metadataController.seqNumber; - break; - case Method.Metadata: - // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property - // It's safe to return altair.Metadata here for all versions - yield this.metadataController.json; - break; - case Method.Goodbye: - yield BigInt(0); - break; - - // Don't bubble Ping, Metadata, and, Goodbye requests to the app layer - - case Method.Status: - yield* this.reqRespHandlers.onStatus(); - break; - case Method.BeaconBlocksByRange: - yield* this.reqRespHandlers.onBeaconBlocksByRange(requestTyped.body); - break; - case Method.BeaconBlocksByRoot: - yield* this.reqRespHandlers.onBeaconBlocksByRoot(requestTyped.body); - break; - case Method.LightClientBootstrap: - yield* this.reqRespHandlers.onLightClientBootstrap(requestTyped.body); - break; - case Method.LightClientOptimisticUpdate: - yield* this.reqRespHandlers.onLightClientOptimisticUpdate(); - break; - case Method.LightClientFinalityUpdate: - yield* this.reqRespHandlers.onLightClientFinalityUpdate(); - break; - case Method.LightClientUpdate: - yield* this.reqRespHandlers.onLightClientUpdatesByRange(requestTyped.body); - break; - default: - throw Error(`Unsupported method ${protocol.method}`); + private async *onBeaconBlocksByRoot( + req: phase0.BeaconBlocksByRootRequest, + peerId: PeerId + ): AsyncIterable> { + if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); } - - // Allow onRequest to return and close the stream - // For Goodbye there may be a race condition where the listener of `receivedGoodbye` - // disconnects in the same syncronous call, preventing the stream from ending cleanly - setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, requestTyped, peerId), 0); + yield* this.reqRespHandlers.onBeaconBlocksByRoot(req); } } diff --git a/packages/beacon-node/src/network/reqresp/reqRespProtocol.ts b/packages/beacon-node/src/network/reqresp/reqRespProtocol.ts new file mode 100644 index 000000000000..f72b163ce096 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/reqRespProtocol.ts @@ -0,0 +1,174 @@ +import {setMaxListeners} from "node:events"; +import {Libp2p} from "libp2p"; +import {PeerId} from "@libp2p/interface-peer-id"; +import {Connection, Stream} from "@libp2p/interface-connection"; +import {IBeaconConfig} from "@lodestar/config"; +import {ILogger} from "@lodestar/utils"; +import {timeoutOptions} from "../../constants/index.js"; +import {PeersData} from "../peers/peersData.js"; +import {IPeerRpcScoreStore} from "../peers/score.js"; +import {IMetrics} from "../../metrics/metrics.js"; +import {sendRequest} from "./request/index.js"; +import {handleRequest} from "./response/index.js"; +import {formatProtocolID} from "./utils/index.js"; +import {RequestError, RequestErrorCode} from "./request/index.js"; +import {Encoding, ProtocolDefinition} from "./types.js"; + +export type IReqRespOptions = Partial; +type ProtocolID = string; + +export interface ReqRespProtocolModules { + config: IBeaconConfig; + libp2p: Libp2p; + peersData: PeersData; + logger: ILogger; + peerRpcScores: IPeerRpcScoreStore; + metrics: IMetrics | null; +} + +/** + * Implementation of Ethereum Consensus p2p Req/Resp domain. + * For the spec that this code is based on, see: + * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain + * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain + */ +export class ReqRespProtocol { + private libp2p: Libp2p; + private readonly peersData: PeersData; + private logger: ILogger; + private controller = new AbortController(); + private options?: IReqRespOptions; + private reqCount = 0; + private respCount = 0; + private metrics: IMetrics["reqResp"] | null; + /** `${protocolPrefix}/${method}/${version}/${encoding}` */ + private readonly supportedProtocols = new Map(); + + constructor(modules: ReqRespProtocolModules, options: IReqRespOptions) { + this.libp2p = modules.libp2p; + this.peersData = modules.peersData; + this.logger = modules.logger; + this.options = options; + this.metrics = modules.metrics?.reqResp ?? null; + } + + registerProtocol(protocol: ProtocolDefinition): void { + const {method, version, encoding} = protocol; + const protocolID = formatProtocolID(method, version, encoding); + this.supportedProtocols.set(protocolID, protocol as ProtocolDefinition); + } + + async start(): Promise { + this.controller = new AbortController(); + // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 + // Since it is perfectly fine to have listeners > 10 + setMaxListeners(Infinity, this.controller.signal); + + for (const [protocolID, protocol] of this.supportedProtocols) { + await this.libp2p.handle(protocolID, this.getRequestHandler(protocol)); + } + } + + async stop(): Promise { + for (const protocolID of this.supportedProtocols.keys()) { + await this.libp2p.unhandle(protocolID); + } + this.controller.abort(); + } + + // Helper to reduce code duplication + protected async sendRequest( + peerId: PeerId, + method: string, + versions: string[], + body: Req, + maxResponses = 1 + ): Promise { + const peerClient = this.peersData.getPeerKind(peerId.toString()); + this.metrics?.outgoingRequests.inc({method}); + const timer = this.metrics?.outgoingRequestRoundtripTime.startTimer({method}); + + // Remember prefered encoding + const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; + + const protocols: ProtocolDefinition[] = []; + for (const version of versions) { + const protocolID = formatProtocolID(method, version, encoding); + const protocol = this.supportedProtocols.get(protocolID); + if (!protocol) { + throw Error(`Request to send to protocol ${protocolID} but it has not been declared`); + } + protocols.push(protocol); + } + + try { + const result = await sendRequest( + {logger: this.logger, libp2p: this.libp2p, peerClient}, + peerId, + protocols, + body, + maxResponses, + this.controller.signal, + this.options, + this.reqCount++ + ); + + return result; + } catch (e) { + this.metrics?.outgoingErrors.inc({method}); + + if (e instanceof RequestError) { + if (e.type.code === RequestErrorCode.DIAL_ERROR || e.type.code === RequestErrorCode.DIAL_TIMEOUT) { + this.metrics?.dialErrors.inc(); + } + + this.onOutgoingReqRespError(peerId, method, e); + } + + throw e; + } finally { + timer?.(); + } + } + + private getRequestHandler(protocol: ProtocolDefinition) { + return async ({connection, stream}: {connection: Connection; stream: Stream}) => { + const peerId = connection.remotePeer; + const peerClient = this.peersData.getPeerKind(peerId.toString()); + const method = protocol.method; + + this.metrics?.incomingRequests.inc({method}); + const timer = this.metrics?.incomingRequestHandlerTime.startTimer({method}); + + this.onIncomingRequest?.(peerId, method); + + try { + await handleRequest({ + logger: this.logger, + stream, + peerId, + protocol, + signal: this.controller.signal, + requestId: this.respCount++, + peerClient, + }); + // TODO: Do success peer scoring here + } catch { + this.metrics?.incomingErrors.inc({method}); + + // TODO: Do error peer scoring here + // Must not throw since this is an event handler + } finally { + timer?.(); + } + }; + } + + protected onIncomingRequest(_peerId: PeerId, _method: string): void { + // Override + } + + protected onOutgoingReqRespError(_peerId: PeerId, _method: string, _error: RequestError): void { + // Override + } +} diff --git a/packages/beacon-node/src/network/reqresp/request/collectResponses.ts b/packages/beacon-node/src/network/reqresp/request/collectResponses.ts index b86b4b1e1af3..018354390f87 100644 --- a/packages/beacon-node/src/network/reqresp/request/collectResponses.ts +++ b/packages/beacon-node/src/network/reqresp/request/collectResponses.ts @@ -1,4 +1,4 @@ -import {Method, isSingleResponseChunkByMethod, IncomingResponseBody} from "../types.js"; +import {ProtocolDefinition} from "../types.js"; import {RequestErrorCode, RequestInternalError} from "./errors.js"; /** @@ -8,20 +8,20 @@ import {RequestErrorCode, RequestInternalError} from "./errors.js"; * ``` * Note: `response` has zero or more chunks for SSZ-list responses or exactly one chunk for non-list */ -export function collectResponses( - method: Method, +export function collectResponses( + protocol: ProtocolDefinition, maxResponses?: number -): (source: AsyncIterable) => Promise { +): (source: AsyncIterable) => Promise { return async (source) => { - if (isSingleResponseChunkByMethod[method]) { + if (protocol.isSingleResponse) { for await (const response of source) { - return response as T; + return response; } throw new RequestInternalError({code: RequestErrorCode.EMPTY_RESPONSE}); } // else: zero or more responses - const responses: IncomingResponseBody[] = []; + const responses: T[] = []; for await (const response of source) { responses.push(response); @@ -29,6 +29,6 @@ export function collectResponses( - {logger, forkDigestContext, libp2p, peersData}: SendRequestModules, +export async function sendRequest( + {logger, libp2p, peerClient}: SendRequestModules, peerId: PeerId, - method: Method, - encoding: Encoding, - versions: Version[], - requestBody: RequestBody, + protocols: ProtocolDefinition[], + requestBody: Req, maxResponses: number, signal?: AbortSignal, options?: Partial, requestId = 0 -): Promise { +): Promise { + if (protocols.length === 0) { + throw Error("sendRequest must set > 0 protocols"); + } + const {REQUEST_TIMEOUT, DIAL_TIMEOUT} = {...timeoutOptions, ...options}; const peerIdStr = peerId.toString(); const peerIdStrShort = prettyPrintPeerId(peerId); - const client = peersData.getPeerKind(peerIdStr); - const logCtx = {method, encoding, client, peer: peerIdStrShort, requestId}; + const {method, encoding} = protocols[0]; + const logCtx = {method, encoding, client: peerClient, peer: peerIdStrShort, requestId}; if (signal?.aborted) { throw new ErrorAborted("sendRequest"); @@ -70,8 +69,8 @@ export async function sendRequest( - versions.map((version) => [formatProtocolId(method, version, encoding), {method, version, encoding}]) + const protocolsMap = new Map( + protocols.map((protocol) => [formatProtocolID(protocol.method, protocol.version, protocol.encoding), protocol]) ); // As of October 2020 we can't rely on libp2p.dialProtocol timeout to work so @@ -87,7 +86,7 @@ export async function sendRequest { - const protocolIds = Array.from(protocols.keys()); + const protocolIds = Array.from(protocolsMap.keys()); const conn = await libp2p.dialProtocol(peerId, protocolIds, {signal: timeoutAndParentSignal}); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!conn) throw Error("dialProtocol timeout"); @@ -105,10 +104,10 @@ export async function sendRequest AsyncIterable; - -type HandleRequestModules = { - config: IBeaconConfig; +export interface HandleRequestOpts { logger: ILogger; - peersData: PeersData; -}; + stream: Stream; + peerId: PeerId; + protocol: ProtocolDefinition; + signal?: AbortSignal; + requestId?: number; + /** Peer client type for logging and metrics: 'prysm' | 'lighthouse' */ + peerClient?: string; +} /** * Handles a ReqResp request from a peer. Throws on error. Logs each step of the response lifecycle. @@ -37,17 +33,16 @@ type HandleRequestModules = { * 4a. Encode and write `` to peer * 4b. On error, encode and write an error `` and stop */ -export async function handleRequest( - {config, logger, peersData: peersData}: HandleRequestModules, - performRequestHandler: PerformRequestHandler, - stream: Stream, - peerId: PeerId, - protocol: Protocol, - signal?: AbortSignal, - requestId = 0 -): Promise { - const client = peersData.getPeerKind(peerId.toString()); - const logCtx = {method: protocol.method, client, peer: prettyPrintPeerId(peerId), requestId}; +export async function handleRequest({ + logger, + stream, + peerId, + protocol, + signal, + requestId = 0, + peerClient = "unknown", +}: HandleRequestOpts): Promise { + const logCtx = {method: protocol.method, client: peerClient, peer: prettyPrintPeerId(peerId), requestId}; let responseError: Error | null = null; await pipe( @@ -68,14 +63,14 @@ export async function handleRequest( } }); - logger.debug("Resp received request", {...logCtx, body: renderRequestBody(protocol.method, requestBody)}); + logger.debug("Resp received request", {...logCtx, body: protocol.renderRequestBody?.(requestBody)}); yield* pipe( - performRequestHandler(protocol, requestBody, peerId), + protocol.handler(requestBody, peerId), // NOTE: Do not log the resp chunk contents, logs get extremely cluttered // Note: Not logging on each chunk since after 1 year it hasn't add any value when debugging // onChunk(() => logger.debug("Resp sending chunk", logCtx)), - responseEncodeSuccess(config, protocol) + responseEncodeSuccess(protocol) ); } catch (e) { const status = e instanceof ResponseError ? e.status : RespStatus.SERVER_ERROR; diff --git a/packages/beacon-node/src/network/reqresp/response/rateLimiter.ts b/packages/beacon-node/src/network/reqresp/response/rateLimiter.ts index a96f77957cc8..8b5d9cd12638 100644 --- a/packages/beacon-node/src/network/reqresp/response/rateLimiter.ts +++ b/packages/beacon-node/src/network/reqresp/response/rateLimiter.ts @@ -4,7 +4,6 @@ import {IMetrics} from "../../../metrics/index.js"; import {IPeerRpcScoreStore, PeerAction} from "../../peers/score.js"; import {IRateLimiter} from "../interface.js"; import {RateTracker} from "../rateTracker.js"; -import {Method, RequestTypedContainer} from "../types.js"; interface IRateLimiterModules { logger: ILogger; @@ -96,7 +95,7 @@ export class InboundRateLimiter implements IRateLimiter { /** * Tracks a request from a peer and returns whether to allow the request based on the configured rate limit params. */ - allowRequest(peerId: PeerId, requestTyped: RequestTypedContainer): boolean { + allowRequest(peerId: PeerId): boolean { const peerIdStr = peerId.toString(); this.lastSeenRequestsByPeer.set(peerIdStr, Date.now()); @@ -114,39 +113,35 @@ export class InboundRateLimiter implements IRateLimiter { return false; } - let numBlock = 0; - switch (requestTyped.method) { - case Method.BeaconBlocksByRange: - numBlock = requestTyped.body.count; - break; - case Method.BeaconBlocksByRoot: - numBlock = requestTyped.body.length; - break; - } + return true; + } + + /** + * Rate limit check for block count + */ + allowBlockByRequest(peerId: PeerId, numBlock: number): boolean { + const peerIdStr = peerId.toString(); + const blockCountPeerTracker = this.blockCountTrackersByPeer.getOrDefault(peerIdStr); - // rate limit check for block count - if (numBlock > 0) { - const blockCountPeerTracker = this.blockCountTrackersByPeer.getOrDefault(peerIdStr); - if (blockCountPeerTracker.requestObjects(numBlock) === 0) { - this.logger.verbose("Do not serve block request due to block count rate limit", { - peerId: peerIdStr, - blockCount: numBlock, - requestsWithinWindow: blockCountPeerTracker.getRequestedObjectsWithinWindow(), - }); - this.peerRpcScores.applyAction(peerId, PeerAction.Fatal, "RateLimit"); - if (this.metrics) { - this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); - } - return false; + if (blockCountPeerTracker.requestObjects(numBlock) === 0) { + this.logger.verbose("Do not serve block request due to block count rate limit", { + peerId: peerIdStr, + blockCount: numBlock, + requestsWithinWindow: blockCountPeerTracker.getRequestedObjectsWithinWindow(), + }); + this.peerRpcScores.applyAction(peerId, PeerAction.Fatal, "RateLimit"); + if (this.metrics) { + this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); } + return false; + } - if (this.blockCountTotalTracker.requestObjects(numBlock) === 0) { - if (this.metrics) { - this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountTotalTracker"}); - } - // don't apply penalty - return false; + if (this.blockCountTotalTracker.requestObjects(numBlock) === 0) { + if (this.metrics) { + this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountTotalTracker"}); } + // don't apply penalty + return false; } return true; diff --git a/packages/beacon-node/src/network/reqresp/types.ts b/packages/beacon-node/src/network/reqresp/types.ts index 25c1356dd246..cff335fdb370 100644 --- a/packages/beacon-node/src/network/reqresp/types.ts +++ b/packages/beacon-node/src/network/reqresp/types.ts @@ -1,5 +1,37 @@ +import {PeerId} from "@libp2p/interface-peer-id"; +import {Type} from "@chainsafe/ssz"; +import {IForkConfig, IForkDigestContext} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; -import {allForks, phase0, ssz, Slot, altair, Root} from "@lodestar/types"; +import {phase0, Slot} from "@lodestar/types"; + +export enum EncodedPayloadType { + ssz, + bytes, +} + +export type EncodedPayload = + | { + type: EncodedPayloadType.ssz; + data: T; + } + | { + type: EncodedPayloadType.bytes; + bytes: Uint8Array; + contextBytes: ContextBytes; + }; + +export type Handler = (requestBody: Req, peerId: PeerId) => AsyncIterable>; + +export interface ProtocolDefinition extends Protocol { + handler: Handler; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + requestType: (fork: ForkName) => Type | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseType: (fork: ForkName) => Type; + renderRequestBody?: (request: Req) => string; + contextBytes: ContextBytesFactory; + isSingleResponse: boolean; +} export const protocolPrefix = "/eth2/beacon_chain/req"; @@ -13,11 +45,30 @@ export enum Method { BeaconBlocksByRange = "beacon_blocks_by_range", BeaconBlocksByRoot = "beacon_blocks_by_root", LightClientBootstrap = "light_client_bootstrap", - LightClientUpdate = "light_client_updates_by_range", + LightClientUpdatesByRange = "light_client_updates_by_range", LightClientFinalityUpdate = "light_client_finality_update", LightClientOptimisticUpdate = "light_client_optimistic_update", } +// To typesafe events to network +type RequestBodyByMethod = { + [Method.Status]: phase0.Status; + [Method.Goodbye]: phase0.Goodbye; + [Method.Ping]: phase0.Ping; + [Method.Metadata]: null; + // Do not matter + [Method.BeaconBlocksByRange]: unknown; + [Method.BeaconBlocksByRoot]: unknown; + [Method.LightClientBootstrap]: unknown; + [Method.LightClientUpdatesByRange]: unknown; + [Method.LightClientFinalityUpdate]: unknown; + [Method.LightClientOptimisticUpdate]: unknown; +}; + +export type RequestTypedContainer = { + [K in Method]: {method: K; body: RequestBodyByMethod[K]}; +}[Method]; + /** RPC Versions */ export enum Version { V1 = "1", @@ -38,220 +89,21 @@ export type Protocol = { encoding: Encoding; }; -export const protocolsSupported: [Method, Version, Encoding][] = [ - [Method.Status, Version.V1, Encoding.SSZ_SNAPPY], - [Method.Goodbye, Version.V1, Encoding.SSZ_SNAPPY], - [Method.Ping, Version.V1, Encoding.SSZ_SNAPPY], - [Method.Metadata, Version.V1, Encoding.SSZ_SNAPPY], - [Method.Metadata, Version.V2, Encoding.SSZ_SNAPPY], - [Method.BeaconBlocksByRange, Version.V1, Encoding.SSZ_SNAPPY], - [Method.BeaconBlocksByRange, Version.V2, Encoding.SSZ_SNAPPY], - [Method.BeaconBlocksByRoot, Version.V1, Encoding.SSZ_SNAPPY], - [Method.BeaconBlocksByRoot, Version.V2, Encoding.SSZ_SNAPPY], - [Method.LightClientBootstrap, Version.V1, Encoding.SSZ_SNAPPY], - [Method.LightClientUpdate, Version.V1, Encoding.SSZ_SNAPPY], - [Method.LightClientFinalityUpdate, Version.V1, Encoding.SSZ_SNAPPY], - [Method.LightClientOptimisticUpdate, Version.V1, Encoding.SSZ_SNAPPY], -]; +export const CONTEXT_BYTES_FORK_DIGEST_LENGTH = 4; -export const isSingleResponseChunkByMethod: {[K in Method]: boolean} = { - [Method.Status]: true, // Exactly 1 response chunk - [Method.Goodbye]: true, - [Method.Ping]: true, - [Method.Metadata]: true, - [Method.BeaconBlocksByRange]: false, // A stream, 0 or more response chunks - [Method.BeaconBlocksByRoot]: false, - [Method.LightClientBootstrap]: true, - [Method.LightClientUpdate]: false, - [Method.LightClientFinalityUpdate]: true, - [Method.LightClientOptimisticUpdate]: true, -}; +export type ContextBytesFactory = + | {type: ContextBytesType.Empty} + | { + type: ContextBytesType.ForkDigest; + forkDigestContext: IForkDigestContext & Pick; + forkFromResponse: (response: Response) => ForkName; + }; + +export type ContextBytes = {type: ContextBytesType.Empty} | {type: ContextBytesType.ForkDigest; forkSlot: Slot}; -export const CONTEXT_BYTES_FORK_DIGEST_LENGTH = 4; export enum ContextBytesType { /** 0 bytes chunk, can be ignored */ Empty, /** A fixed-width 4 byte , set to the ForkDigest matching the chunk: compute_fork_digest(fork_version, genesis_validators_root) */ ForkDigest, } - -/** Meaning of the chunk per protocol */ -export function contextBytesTypeByProtocol(protocol: Protocol): ContextBytesType { - switch (protocol.method) { - case Method.Status: - case Method.Goodbye: - case Method.Ping: - case Method.Metadata: - return ContextBytesType.Empty; - case Method.LightClientBootstrap: - case Method.LightClientUpdate: - case Method.LightClientFinalityUpdate: - case Method.LightClientOptimisticUpdate: - return ContextBytesType.ForkDigest; - case Method.BeaconBlocksByRange: - case Method.BeaconBlocksByRoot: - switch (protocol.version) { - case Version.V1: - return ContextBytesType.Empty; - case Version.V2: - return ContextBytesType.ForkDigest; - } - } -} - -/** Request SSZ type for each method and ForkName */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type -export function getRequestSzzTypeByMethod(method: Method) { - switch (method) { - case Method.Status: - return ssz.phase0.Status; - case Method.Goodbye: - return ssz.phase0.Goodbye; - case Method.Ping: - return ssz.phase0.Ping; - case Method.Metadata: - case Method.LightClientFinalityUpdate: - case Method.LightClientOptimisticUpdate: - return null; - case Method.BeaconBlocksByRange: - return ssz.phase0.BeaconBlocksByRangeRequest; - case Method.BeaconBlocksByRoot: - return ssz.phase0.BeaconBlocksByRootRequest; - case Method.LightClientBootstrap: - return ssz.Root; - case Method.LightClientUpdate: - return ssz.altair.LightClientUpdatesByRange; - } -} - -export type RequestBodyByMethod = { - [Method.Status]: phase0.Status; - [Method.Goodbye]: phase0.Goodbye; - [Method.Ping]: phase0.Ping; - [Method.Metadata]: null; - [Method.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest; - [Method.BeaconBlocksByRoot]: phase0.BeaconBlocksByRootRequest; - [Method.LightClientBootstrap]: Root; - [Method.LightClientUpdate]: altair.LightClientUpdatesByRange; - [Method.LightClientFinalityUpdate]: null; - [Method.LightClientOptimisticUpdate]: null; -}; - -/** Response SSZ type for each method and ForkName */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type -export function getResponseSzzTypeByMethod(protocol: Protocol, forkName: ForkName) { - switch (protocol.method) { - case Method.Status: - return ssz.phase0.Status; - case Method.Goodbye: - return ssz.phase0.Goodbye; - case Method.Ping: - return ssz.phase0.Ping; - case Method.Metadata: { - // V1 -> phase0.Metadata, V2 -> altair.Metadata - const fork = protocol.version === Version.V1 ? ForkName.phase0 : ForkName.altair; - return ssz[fork].Metadata; - } - case Method.BeaconBlocksByRange: - case Method.BeaconBlocksByRoot: - // SignedBeaconBlock type is changed in altair - return ssz[forkName].SignedBeaconBlock; - case Method.LightClientBootstrap: - return ssz.altair.LightClientBootstrap; - case Method.LightClientUpdate: - return ssz.altair.LightClientUpdate; - case Method.LightClientFinalityUpdate: - return ssz.altair.LightClientFinalityUpdate; - case Method.LightClientOptimisticUpdate: - return ssz.altair.LightClientOptimisticUpdate; - } -} - -/** Return either an ssz type or the serializer for ReqRespBlockResponse */ -export function getOutgoingSerializerByMethod(protocol: Protocol): OutgoingSerializer { - switch (protocol.method) { - case Method.Status: - return ssz.phase0.Status; - case Method.Goodbye: - return ssz.phase0.Goodbye; - case Method.Ping: - return ssz.phase0.Ping; - case Method.Metadata: { - // V1 -> phase0.Metadata, V2 -> altair.Metadata - const fork = protocol.version === Version.V1 ? ForkName.phase0 : ForkName.altair; - return ssz[fork].Metadata; - } - case Method.BeaconBlocksByRange: - case Method.BeaconBlocksByRoot: - return reqRespBlockResponseSerializer; - case Method.LightClientBootstrap: - return ssz.altair.LightClientBootstrap; - case Method.LightClientUpdate: - return ssz.altair.LightClientUpdate; - case Method.LightClientFinalityUpdate: - return ssz.altair.LightClientFinalityUpdate; - case Method.LightClientOptimisticUpdate: - return ssz.altair.LightClientOptimisticUpdate; - } -} - -type CommonResponseBodyByMethod = { - [Method.Status]: phase0.Status; - [Method.Goodbye]: phase0.Goodbye; - [Method.Ping]: phase0.Ping; - [Method.Metadata]: phase0.Metadata; - [Method.LightClientBootstrap]: altair.LightClientBootstrap; - [Method.LightClientUpdate]: altair.LightClientUpdate; - [Method.LightClientFinalityUpdate]: altair.LightClientFinalityUpdate; - [Method.LightClientOptimisticUpdate]: altair.LightClientOptimisticUpdate; -}; - -// Used internally by lodestar to response to beacon_blocks_by_range and beacon_blocks_by_root -// without having to deserialize and serialize from/to bytes -export type OutgoingResponseBodyByMethod = CommonResponseBodyByMethod & { - [Method.BeaconBlocksByRange]: ReqRespBlockResponse; - [Method.BeaconBlocksByRoot]: ReqRespBlockResponse; -}; - -// p2p protocol in the spec -export type IncomingResponseBodyByMethod = CommonResponseBodyByMethod & { - [Method.BeaconBlocksByRange]: allForks.SignedBeaconBlock; - [Method.BeaconBlocksByRoot]: allForks.SignedBeaconBlock; -}; - -// Helper types to generically define the arguments of the encoder functions - -export type RequestBody = RequestBodyByMethod[Method]; -export type OutgoingResponseBody = OutgoingResponseBodyByMethod[Method]; -export type IncomingResponseBody = IncomingResponseBodyByMethod[Method]; -export type RequestOrIncomingResponseBody = RequestBody | IncomingResponseBody; -export type RequestOrOutgoingResponseBody = RequestBody | OutgoingResponseBody; - -export type RequestType = Exclude, null>; -export type ResponseType = ReturnType; -export type RequestOrResponseType = RequestType | ResponseType; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type OutgoingSerializer = {serialize: (body: any) => Uint8Array}; - -// Link each method with its type for more type-safe event handlers - -export type RequestTypedContainer = { - [K in Method]: {method: K; body: RequestBodyByMethod[K]}; -}[Method]; -export type ResponseTypedContainer = { - [K in Method]: {method: K; body: OutgoingResponseBodyByMethod[K]}; -}[Method]; - -/** Serializer for ReqRespBlockResponse */ -export const reqRespBlockResponseSerializer = { - serialize: (chunk: ReqRespBlockResponse): Uint8Array => { - return chunk.bytes; - }, -}; - -/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ -export type ReqRespBlockResponse = { - /** Deserialized data of allForks.SignedBeaconBlock */ - bytes: Uint8Array; - slot: Slot; -}; diff --git a/packages/beacon-node/src/network/reqresp/utils/index.ts b/packages/beacon-node/src/network/reqresp/utils/index.ts index cbe56f2b990d..b5799cd88d46 100644 --- a/packages/beacon-node/src/network/reqresp/utils/index.ts +++ b/packages/beacon-node/src/network/reqresp/utils/index.ts @@ -3,4 +3,3 @@ export * from "./bufferedSource.js"; export * from "./errorMessage.js"; export * from "./onChunk.js"; export * from "./protocolId.js"; -export {renderRequestBody} from "./renderRequestBody.js"; diff --git a/packages/beacon-node/src/network/reqresp/utils/protocolId.ts b/packages/beacon-node/src/network/reqresp/utils/protocolId.ts index 3d4314e27d1d..c88ba171f80e 100644 --- a/packages/beacon-node/src/network/reqresp/utils/protocolId.ts +++ b/packages/beacon-node/src/network/reqresp/utils/protocolId.ts @@ -1,27 +1,10 @@ -import {Method, Version, Encoding, protocolPrefix, Protocol} from "../types.js"; - -const methods = new Set(Object.values(Method)); -const versions = new Set(Object.values(Version)); -const encodings = new Set(Object.values(Encoding)); - -/** Render protocol ID */ -export function formatProtocolId(method: Method, version: Version, encoding: Encoding): string { +import {Encoding, protocolPrefix} from "../types.js"; + +/** + * @param method `"beacon_blocks_by_range"` + * @param version `"1"` + * @param encoding `"ssz_snappy"` + */ +export function formatProtocolID(method: string, version: string, encoding: Encoding): string { return `${protocolPrefix}/${method}/${version}/${encoding}`; } - -export function parseProtocolId(protocolId: string): Protocol { - if (!protocolId.startsWith(protocolPrefix)) { - throw Error(`Unknown protocolId prefix: ${protocolId}`); - } - - // +1 for the first "/" - const suffix = protocolId.slice(protocolPrefix.length + 1); - - const [method, version, encoding] = suffix.split("/") as [Method, Version, Encoding]; - - if (!method || !methods.has(method)) throw Error(`Unknown protocolId method ${method}`); - if (!version || !versions.has(version)) throw Error(`Unknown protocolId version ${version}`); - if (!encoding || !encodings.has(encoding)) throw Error(`Unknown protocolId encoding ${encoding}`); - - return {method, version, encoding}; -} diff --git a/packages/beacon-node/src/network/reqresp/utils/renderRequestBody.ts b/packages/beacon-node/src/network/reqresp/utils/renderRequestBody.ts deleted file mode 100644 index 823be579f55f..000000000000 --- a/packages/beacon-node/src/network/reqresp/utils/renderRequestBody.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {toHexString} from "@lodestar/utils"; -import {Method, RequestBody, RequestBodyByMethod} from "../types.js"; - -/** - * Render requestBody as a succint string for debug purposes - */ -export function renderRequestBody(method: Method, requestBody: RequestBody): string { - switch (method) { - case Method.Status: - // Don't log any data - return ""; - - case Method.Goodbye: - return (requestBody as RequestBodyByMethod[Method.Goodbye]).toString(10); - - case Method.Ping: - return (requestBody as RequestBodyByMethod[Method.Ping]).toString(10); - - case Method.Metadata: - case Method.LightClientFinalityUpdate: - case Method.LightClientOptimisticUpdate: - return "null"; - - case Method.BeaconBlocksByRange: { - const range = requestBody as RequestBodyByMethod[Method.BeaconBlocksByRange]; - return `${range.startSlot},${range.step},${range.count}`; - } - - case Method.BeaconBlocksByRoot: - return ((requestBody as RequestBodyByMethod[Method.BeaconBlocksByRoot]) as Uint8Array[]) - .map((root) => toHexString(root)) - .join(","); - - case Method.LightClientBootstrap: - return toHexString((requestBody as RequestBodyByMethod[Method.LightClientBootstrap]) as Uint8Array); - - case Method.LightClientUpdate: { - const updateRequest = requestBody as RequestBodyByMethod[Method.LightClientUpdate]; - return `${updateRequest.startPeriod},${updateRequest.count}`; - } - } -} diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts index 098a8572f5f0..f3a5912c1534 100644 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts @@ -27,7 +27,7 @@ describe("network / reqresp / encoders / responseTypes", () => { [Method.BeaconBlocksByRange]: [generateEmptySignedBlocks(2)], [Method.BeaconBlocksByRoot]: [generateEmptySignedBlocks(2)], [Method.LightClientBootstrap]: [[ssz.altair.LightClientBootstrap.defaultValue()]], - [Method.LightClientUpdate]: [[ssz.altair.LightClientUpdate.defaultValue()]], + [Method.LightClientUpdatesByRange]: [[ssz.altair.LightClientUpdate.defaultValue()]], [Method.LightClientFinalityUpdate]: [[ssz.altair.LightClientFinalityUpdate.defaultValue()]], [Method.LightClientOptimisticUpdate]: [[ssz.altair.LightClientOptimisticUpdate.defaultValue()]], }; diff --git a/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts b/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts index e2e5053f7d90..891428096e27 100644 --- a/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts @@ -17,7 +17,7 @@ import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; import {getValidPeerId} from "../../../../utils/peer.js"; import {testLogger} from "../../../../utils/logger.js"; import {sszSnappySignedBeaconBlockPhase0} from "../encodingStrategies/sszSnappy/testData.js"; -import {formatProtocolId} from "../../../../../src/network/reqresp/utils/protocolId.js"; +import {formatProtocolID} from "../../../../../src/network/reqresp/utils/protocolId.js"; /* eslint-disable require-yield */ @@ -41,7 +41,7 @@ describe("network / reqresp / request / responseTimeoutsHandler", () => { Buffer.from([RespStatus.SUCCESS]), ...sszSnappySignedBeaconBlockPhase0.chunks.map((chunk) => chunk.subarray()), ]); - const protocol = formatProtocolId(method, version, encoding); + const protocol = formatProtocolID(method, version, encoding); const peerId = getValidPeerId(); const metadata: IRequestErrorMetadata = {method, encoding, peer: peerId.toString()}; diff --git a/packages/beacon-node/test/unit/network/util.test.ts b/packages/beacon-node/test/unit/network/util.test.ts index b808e19081bf..938ac05ee831 100644 --- a/packages/beacon-node/test/unit/network/util.test.ts +++ b/packages/beacon-node/test/unit/network/util.test.ts @@ -4,9 +4,9 @@ import {createSecp256k1PeerId} from "@libp2p/peer-id-factory"; import {config} from "@lodestar/config/default"; import {ForkName} from "@lodestar/params"; import {ENR} from "@chainsafe/discv5"; -import {Method, Version, Encoding} from "../../../src/network/reqresp/types.js"; +import {Method, Version, Encoding, Protocol, protocolPrefix} from "../../../src/network/reqresp/types.js"; import {defaultNetworkOptions} from "../../../src/network/options.js"; -import {formatProtocolId, parseProtocolId} from "../../../src/network/reqresp/utils/index.js"; +import {formatProtocolID} from "../../../src/network/reqresp/utils/index.js"; import {createNodeJsLibp2p, isLocalMultiAddr} from "../../../src/network/index.js"; import {getCurrentAndNextFork} from "../../../src/network/forks.js"; @@ -45,13 +45,24 @@ describe("ReqResp protocolID parse / render", () => { for (const {method, encoding, version, protocolId} of testCases) { it(`Should render ${protocolId}`, () => { - expect(formatProtocolId(method, version, encoding)).to.equal(protocolId); + expect(formatProtocolID(method, version, encoding)).to.equal(protocolId); }); it(`Should parse ${protocolId}`, () => { expect(parseProtocolId(protocolId)).to.deep.equal({method, version, encoding}); }); } + + function parseProtocolId(protocolId: string): Protocol { + if (!protocolId.startsWith(protocolPrefix)) { + throw Error(`Unknown protocolId prefix: ${protocolId}`); + } + + // +1 for the first "/" + const suffix = protocolId.slice(protocolPrefix.length + 1); + const [method, version, encoding] = suffix.split("/") as [Method, Version, Encoding]; + return {method, version, encoding}; + } }); describe("getCurrentAndNextFork", function () { From fe8021b0986f9e50c8687c5848d37343df802da0 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 16 Nov 2022 01:25:30 +0100 Subject: [PATCH 02/23] Add package skeleton for @lodestar/reqresp --- packages/reqresp/.babel-register | 15 ++ packages/reqresp/.gitignore | 10 ++ packages/reqresp/.mocharc.yaml | 8 ++ packages/reqresp/.nycrc.json | 3 + packages/reqresp/LICENSE | 201 +++++++++++++++++++++++++++ packages/reqresp/README.md | 48 +++++++ packages/reqresp/package.json | 80 +++++++++++ packages/reqresp/test/setup.ts | 6 + packages/reqresp/tsconfig.build.json | 7 + packages/reqresp/tsconfig.json | 4 + 10 files changed, 382 insertions(+) create mode 100644 packages/reqresp/.babel-register create mode 100644 packages/reqresp/.gitignore create mode 100644 packages/reqresp/.mocharc.yaml create mode 100644 packages/reqresp/.nycrc.json create mode 100644 packages/reqresp/LICENSE create mode 100644 packages/reqresp/README.md create mode 100644 packages/reqresp/package.json create mode 100644 packages/reqresp/test/setup.ts create mode 100644 packages/reqresp/tsconfig.build.json create mode 100644 packages/reqresp/tsconfig.json diff --git a/packages/reqresp/.babel-register b/packages/reqresp/.babel-register new file mode 100644 index 000000000000..35d91b6f385e --- /dev/null +++ b/packages/reqresp/.babel-register @@ -0,0 +1,15 @@ +/* + See + https://github.com/babel/babel/issues/8652 + https://github.com/babel/babel/pull/6027 + Babel isn't currently configured by default to read .ts files and + can only be configured to do so via cli or configuration below. + + This file is used by mocha to interpret test files using a properly + configured babel. + + This can (probably) be removed in babel 8.x. +*/ +require('@babel/register')({ + extensions: ['.ts'], +}) diff --git a/packages/reqresp/.gitignore b/packages/reqresp/.gitignore new file mode 100644 index 000000000000..668e0a04c0b4 --- /dev/null +++ b/packages/reqresp/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +lib +.nyc_output/ +coverage/** +.DS_Store +*.swp +.idea +yarn-error.log +package-lock.json +dist* diff --git a/packages/reqresp/.mocharc.yaml b/packages/reqresp/.mocharc.yaml new file mode 100644 index 000000000000..f9375365e517 --- /dev/null +++ b/packages/reqresp/.mocharc.yaml @@ -0,0 +1,8 @@ +colors: true +timeout: 2000 +exit: true +extension: ["ts"] +require: + - ./test/setup.ts +node-option: + - "loader=ts-node/esm" diff --git a/packages/reqresp/.nycrc.json b/packages/reqresp/.nycrc.json new file mode 100644 index 000000000000..69aa626339a0 --- /dev/null +++ b/packages/reqresp/.nycrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.nycrc.json" +} diff --git a/packages/reqresp/LICENSE b/packages/reqresp/LICENSE new file mode 100644 index 000000000000..f49a4e16e68b --- /dev/null +++ b/packages/reqresp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/reqresp/README.md b/packages/reqresp/README.md new file mode 100644 index 000000000000..059d5df34b5f --- /dev/null +++ b/packages/reqresp/README.md @@ -0,0 +1,48 @@ +# Lodestar Eth Consensus Req/Resp Protocol + +[![Discord](https://img.shields.io/discord/593655374469660673.svg?label=Discord&logo=discord)](https://discord.gg/aMxzVcr) +[![ETH Beacon APIs Spec v2.1.0](https://img.shields.io/badge/ETH%20beacon--APIs-2.1.0-blue)](https://github.com/ethereum/beacon-APIs/releases/tag/v2.1.0) +![ES Version](https://img.shields.io/badge/ES-2020-yellow) +![Node Version](https://img.shields.io/badge/node-16.x-green) + +> This package is part of [ChainSafe's Lodestar](https://lodestar.chainsafe.io) project + +Typescript REST client for the [Ethereum Consensus API spec](https://github.com/ethereum/beacon-apis) + +## Usage + +```typescript +import {getClient} from "@lodestar/api"; +import {config} from "@lodestar/config/default"; + +const api = getClient({baseUrl: "http://localhost:9596"}, {config}); + +api.beacon + .getStateValidator( + "head", + "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95" + ) + .then((res) => console.log("Your balance is:", res.data.balance)); +``` + +## Prerequisites + +- [Lerna](https://github.com/lerna/lerna) +- [Yarn](https://yarnpkg.com/) + +## What you need + +You will need to go over the [specification](https://github.com/ethereum/beacon-apis). You will also need to have a [basic understanding of sharding](https://eth.wiki/sharding/Sharding-FAQs). + +## Getting started + +- Follow the [installation guide](https://chainsafe.github.io/lodestar/) to install Lodestar. +- Quickly try out the whole stack by [starting a local testnet](https://chainsafe.github.io/lodestar/usage/local). + +## Contributors + +Read our [contributors document](/CONTRIBUTING.md), [submit an issue](https://github.com/ChainSafe/lodestar/issues/new/choose) or talk to us on our [discord](https://discord.gg/yjyvFRP)! + +## License + +Apache-2.0 [ChainSafe Systems](https://chainsafe.io) diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json new file mode 100644 index 000000000000..ffa2da63156a --- /dev/null +++ b/packages/reqresp/package.json @@ -0,0 +1,80 @@ +{ + "name": "@lodestar/reqresp", + "description": "A Typescript implementation of the Ethereum Consensus Req/Resp protocol", + "license": "Apache-2.0", + "author": "ChainSafe Systems", + "homepage": "https://github.com/ChainSafe/lodestar#readme", + "repository": { + "type": "git", + "url": "git+https://github.com:ChainSafe/lodestar.git" + }, + "bugs": { + "url": "https://github.com/ChainSafe/lodestar/issues" + }, + "version": "1.2.1", + "type": "module", + "exports": { + ".": { + "import": "./lib/index.js" + }, + }, + "typesVersions": { + "*": { + "*": [ + "*", + "lib/*", + "lib/*/index" + ] + } + }, + "types": "./lib/index.d.ts", + "files": [ + "lib/**/*.d.ts", + "lib/**/*.js", + "lib/**/*.js.map", + "*.d.ts", + "*.js" + ], + "scripts": { + "clean": "rm -rf lib && rm -f *.tsbuildinfo", + "build": "tsc -p tsconfig.build.json", + "build:release": "yarn clean && yarn run build", + "check-build": "node -e \"(async function() { await import('./lib/index.js') })()\"", + "check-types": "tsc", + "coverage": "codecov -F lodestar-api", + "lint": "eslint --color --ext .ts src/ test/", + "lint:fix": "yarn run lint --fix", + "pretest": "yarn run check-types", + "test": "yarn test:unit && yarn test:e2e", + "test:unit": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'", + "check-readme": "typescript-docs-verifier" + }, + "dependencies": { + "@chainsafe/persistent-merkle-tree": "^0.4.2", + "@chainsafe/ssz": "^0.9.2", + "@lodestar/config": "^1.2.1", + "@lodestar/params": "^1.2.1", + "@lodestar/types": "^1.2.1", + "@lodestar/utils": "^1.2.1", + "cross-fetch": "^3.1.4", + "eventsource": "^2.0.2", + "qs": "^6.10.1" + }, + "devDependencies": { + "@types/eventsource": "^1.1.5", + "@types/qs": "^6.9.6", + "ajv": "^8.11.0", + "fastify": "3.15.1" + }, + "peerDependencies": { + "fastify": "3.15.1" + }, + "keywords": [ + "ethereum", + "eth-consensus", + "beacon", + "p2p", + "reqresp", + "blockchain" + ] +} diff --git a/packages/reqresp/test/setup.ts b/packages/reqresp/test/setup.ts new file mode 100644 index 000000000000..b83e6cb78511 --- /dev/null +++ b/packages/reqresp/test/setup.ts @@ -0,0 +1,6 @@ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import sinonChai from "sinon-chai"; + +chai.use(chaiAsPromised); +chai.use(sinonChai); diff --git a/packages/reqresp/tsconfig.build.json b/packages/reqresp/tsconfig.build.json new file mode 100644 index 000000000000..92235557ba5d --- /dev/null +++ b/packages/reqresp/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib" + } +} diff --git a/packages/reqresp/tsconfig.json b/packages/reqresp/tsconfig.json new file mode 100644 index 000000000000..b29a7b46c4b1 --- /dev/null +++ b/packages/reqresp/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {} +} From 4cf1ca36f45e37a24c6a2be061f052794fb5f66d Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 16 Nov 2022 03:40:06 +0100 Subject: [PATCH 03/23] Fix all dependencies and file reference issues in reqresp --- .../beacon-node/src/constants/constants.ts | 1 - packages/beacon-node/src/constants/network.ts | 38 ----- .../beacon-node/src/metrics/metrics/beacon.ts | 44 ------ packages/reqresp/package.json | 32 ++-- packages/reqresp/src/constants.ts | 12 ++ .../src}/encoders/requestDecode.ts | 0 .../src}/encoders/requestEncode.ts | 0 .../src}/encoders/responseDecode.ts | 2 +- .../src}/encoders/responseEncode.ts | 2 +- .../src}/encodingStrategies/index.ts | 0 .../encodingStrategies/sszSnappy/decode.ts | 2 +- .../encodingStrategies/sszSnappy/encode.ts | 0 .../encodingStrategies/sszSnappy/errors.ts | 0 .../encodingStrategies/sszSnappy/index.ts | 0 .../sszSnappy/snappyFrames/uncompress.ts | 0 .../encodingStrategies/sszSnappy/utils.ts | 0 .../src}/handlers/beaconBlocksByRange.ts | 5 +- .../src}/handlers/beaconBlocksByRoot.ts | 5 +- .../reqresp => reqresp/src}/handlers/index.ts | 3 +- .../src}/handlers/lightClientBootstrap.ts | 5 +- .../handlers/lightClientFinalityUpdate.ts | 4 +- .../handlers/lightClientOptimisticUpdate.ts | 4 +- .../handlers/lightClientUpdatesByRange.ts | 5 +- .../network/reqresp => reqresp/src}/index.ts | 1 + .../reqresp => reqresp/src}/interface.ts | 36 ++++- packages/reqresp/src/metrics.ts | 129 +++++++++++++++ .../reqresp => reqresp/src}/rateTracker.ts | 0 .../reqresp => reqresp/src}/reqResp.ts | 7 +- .../src}/reqRespProtocol.ts | 13 +- .../src}/request/collectResponses.ts | 0 .../reqresp => reqresp/src}/request/errors.ts | 2 +- .../reqresp => reqresp/src}/request/index.ts | 6 +- .../src}/response/errors.ts | 2 +- .../reqresp => reqresp/src}/response/index.ts | 5 +- .../src}/response/rateLimiter.ts | 14 +- .../network/reqresp => reqresp/src}/score.ts | 2 +- packages/reqresp/src/sharedTypes.ts | 149 ++++++++++++++++++ .../network/reqresp => reqresp/src}/types.ts | 0 .../src/utils}/abortableSource.ts | 0 .../utils/assertSequentialBlocksInRange.ts | 0 .../src}/utils/bufferedSource.ts | 0 .../src}/utils/errorMessage.ts | 0 .../reqresp => reqresp/src}/utils/index.ts | 3 + packages/reqresp/src/utils/multifork.ts | 9 ++ .../reqresp => reqresp/src}/utils/onChunk.ts | 0 packages/reqresp/src/utils/peerId.ts | 6 + .../src}/utils/protocolId.ts | 0 packages/reqresp/tsconfig.build.json | 3 +- packages/reqresp/tsconfig.json | 3 +- 49 files changed, 399 insertions(+), 155 deletions(-) create mode 100644 packages/reqresp/src/constants.ts rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encoders/requestDecode.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encoders/requestEncode.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encoders/responseDecode.ts (98%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encoders/responseEncode.ts (98%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/index.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/sszSnappy/decode.ts (98%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/sszSnappy/encode.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/sszSnappy/errors.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/sszSnappy/index.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/encodingStrategies/sszSnappy/utils.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/beaconBlocksByRange.ts (96%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/beaconBlocksByRoot.ts (87%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/index.ts (95%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/lightClientBootstrap.ts (77%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/lightClientFinalityUpdate.ts (84%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/lightClientOptimisticUpdate.ts (84%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/handlers/lightClientUpdatesByRange.ts (82%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/index.ts (91%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/interface.ts (63%) create mode 100644 packages/reqresp/src/metrics.ts rename packages/{beacon-node/src/network/reqresp => reqresp/src}/rateTracker.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/reqResp.ts (98%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/reqRespProtocol.ts (94%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/request/collectResponses.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/request/errors.ts (98%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/request/index.ts (97%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/response/errors.ts (92%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/response/index.ts (96%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/response/rateLimiter.ts (92%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/score.ts (97%) create mode 100644 packages/reqresp/src/sharedTypes.ts rename packages/{beacon-node/src/network/reqresp => reqresp/src}/types.ts (100%) rename packages/{beacon-node/src/util => reqresp/src/utils}/abortableSource.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/utils/assertSequentialBlocksInRange.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/utils/bufferedSource.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/utils/errorMessage.ts (100%) rename packages/{beacon-node/src/network/reqresp => reqresp/src}/utils/index.ts (65%) create mode 100644 packages/reqresp/src/utils/multifork.ts rename packages/{beacon-node/src/network/reqresp => reqresp/src}/utils/onChunk.ts (100%) create mode 100644 packages/reqresp/src/utils/peerId.ts rename packages/{beacon-node/src/network/reqresp => reqresp/src}/utils/protocolId.ts (100%) diff --git a/packages/beacon-node/src/constants/constants.ts b/packages/beacon-node/src/constants/constants.ts index 5eacbac83e12..3587114e33ce 100644 --- a/packages/beacon-node/src/constants/constants.ts +++ b/packages/beacon-node/src/constants/constants.ts @@ -7,7 +7,6 @@ export const ZERO_HASH = Buffer.alloc(32, 0); export const ZERO_HASH_HEX = "0x" + "00".repeat(32); export const EMPTY_SIGNATURE = Buffer.alloc(96, 0); export const GRAFFITI_SIZE = 32; -export const MAX_VARINT_BYTES = 10; /** * The maximum milliseconds of clock disparity assumed between honest nodes. diff --git a/packages/beacon-node/src/constants/network.ts b/packages/beacon-node/src/constants/network.ts index 84932ac4c218..0f3cff4e8fc4 100644 --- a/packages/beacon-node/src/constants/network.ts +++ b/packages/beacon-node/src/constants/network.ts @@ -12,50 +12,12 @@ */ export const ATTESTATION_PROPAGATION_SLOT_RANGE = 32; -// Request/Response constants - -export enum RespStatus { - /** - * A normal response follows, with contents matching the expected message schema and encoding specified in the request - */ - SUCCESS = 0, - /** - * The contents of the request are semantically invalid, or the payload is malformed, - * or could not be understood. The response payload adheres to the ErrorMessage schema - */ - INVALID_REQUEST = 1, - /** - * The responder encountered an error while processing the request. The response payload adheres to the ErrorMessage schema - */ - SERVER_ERROR = 2, - /** - * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema (described below). Note: This response code is only valid as a response to BlocksByRange - */ - RESOURCE_UNAVAILABLE = 3, - /** - * Our node does not have bandwidth to serve requests due to either per-peer quota or total quota. - */ - RATE_LIMITED = 139, -} - -export type RpcResponseStatusError = Exclude; - /** The maximum allowed size of uncompressed gossip messages. */ export const GOSSIP_MAX_SIZE = 2 ** 20; export const GOSSIP_MAX_SIZE_BELLATRIX = 10 * GOSSIP_MAX_SIZE; /** The maximum allowed size of uncompressed req/resp chunked responses. */ export const MAX_CHUNK_SIZE = 2 ** 20; export const MAX_CHUNK_SIZE_BELLATRIX = 10 * MAX_CHUNK_SIZE; -/** The maximum time to wait for first byte of request response (time-to-first-byte). */ -export const TTFB_TIMEOUT = 5 * 1000; // 5 sec -/** The maximum time for complete response transfer. */ -export const RESP_TIMEOUT = 10 * 1000; // 10 sec -/** Non-spec timeout from sending request until write stream closed by responder */ -export const REQUEST_TIMEOUT = 5 * 1000; // 5 sec -/** Non-spec timeout from dialing protocol until stream opened */ -export const DIAL_TIMEOUT = 5 * 1000; // 5 sec -// eslint-disable-next-line @typescript-eslint/naming-convention -export const timeoutOptions = {TTFB_TIMEOUT, RESP_TIMEOUT, REQUEST_TIMEOUT, DIAL_TIMEOUT}; export enum GoodByeReasonCode { CLIENT_SHUTDOWN = 1, diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 9f319dbcd152..a62b513c8283 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -95,50 +95,6 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { buckets: [1, 2, 3, 5, 7, 10, 20, 30, 50, 100], }), - reqResp: { - outgoingRequests: register.gauge<"method">({ - name: "beacon_reqresp_outgoing_requests_total", - help: "Counts total requests done per method", - labelNames: ["method"], - }), - outgoingRequestRoundtripTime: register.histogram<"method">({ - name: "beacon_reqresp_outgoing_request_roundtrip_time_seconds", - help: "Histogram of outgoing requests round-trip time", - labelNames: ["method"], - buckets: [0.1, 0.2, 0.5, 1, 5, 15, 60], - }), - outgoingErrors: register.gauge<"method">({ - name: "beacon_reqresp_outgoing_requests_error_total", - help: "Counts total failed requests done per method", - labelNames: ["method"], - }), - incomingRequests: register.gauge<"method">({ - name: "beacon_reqresp_incoming_requests_total", - help: "Counts total responses handled per method", - labelNames: ["method"], - }), - incomingRequestHandlerTime: register.histogram<"method">({ - name: "beacon_reqresp_incoming_request_handler_time_seconds", - help: "Histogram of incoming requests internal handling time", - labelNames: ["method"], - buckets: [0.1, 0.2, 0.5, 1, 5], - }), - incomingErrors: register.gauge<"method">({ - name: "beacon_reqresp_incoming_requests_error_total", - help: "Counts total failed responses handled per method", - labelNames: ["method"], - }), - dialErrors: register.gauge({ - name: "beacon_reqresp_dial_errors_total", - help: "Count total dial errors", - }), - rateLimitErrors: register.gauge<"tracker">({ - name: "beacon_reqresp_rate_limiter_errors_total", - help: "Count rate limiter errors", - labelNames: ["tracker"], - }), - }, - blockProductionTime: register.histogram({ name: "beacon_block_production_seconds", help: "Full runtime of block production", diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index ffa2da63156a..54080588c140 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/reqresp", - "description": "A Typescript implementation of the Ethereum Consensus Req/Resp protocol", + "description": "A Typescript implementation of the Ethereum Consensus ReqResp protocol", "license": "Apache-2.0", "author": "ChainSafe Systems", "homepage": "https://github.com/ChainSafe/lodestar#readme", @@ -16,7 +16,7 @@ "exports": { ".": { "import": "./lib/index.js" - }, + } }, "typesVersions": { "*": { @@ -50,24 +50,24 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/persistent-merkle-tree": "^0.4.2", - "@chainsafe/ssz": "^0.9.2", - "@lodestar/config": "^1.2.1", - "@lodestar/params": "^1.2.1", + "@libp2p/interface-peer-id": "^1.0.4", + "@libp2p/interface-connection": "^3.0.2", + "@libp2p/interface-peer-id": "^1.0.4", + "strict-event-emitter-types": "^2.0.0", "@lodestar/types": "^1.2.1", "@lodestar/utils": "^1.2.1", - "cross-fetch": "^3.1.4", - "eventsource": "^2.0.2", - "qs": "^6.10.1" - }, - "devDependencies": { - "@types/eventsource": "^1.1.5", - "@types/qs": "^6.9.6", - "ajv": "^8.11.0", - "fastify": "3.15.1" + "@lodestar/params": "^1.2.1", + "@lodestar/config": "^1.2.1", + "@chainsafe/discv5": "^1.4.0", + "@chainsafe/ssz": "^0.9.2", + "stream-to-it": "^0.2.0", + "@chainsafe/snappy-stream": "5.1.1", + "varint": "^6.0.0", + "snappyjs": "^0.7.0", + "uint8arraylist": "^2.3.2" }, "peerDependencies": { - "fastify": "3.15.1" + "libp2p": "0.39.2" }, "keywords": [ "ethereum", diff --git a/packages/reqresp/src/constants.ts b/packages/reqresp/src/constants.ts new file mode 100644 index 000000000000..762d164359ab --- /dev/null +++ b/packages/reqresp/src/constants.ts @@ -0,0 +1,12 @@ +/** The maximum time for complete response transfer. */ +export const RESP_TIMEOUT = 10 * 1000; // 10 sec +/** Non-spec timeout from sending request until write stream closed by responder */ +export const REQUEST_TIMEOUT = 5 * 1000; // 5 sec +/** The maximum time to wait for first byte of request response (time-to-first-byte). */ +export const TTFB_TIMEOUT = 5 * 1000; // 5 sec +/** Non-spec timeout from dialing protocol until stream opened */ +export const DIAL_TIMEOUT = 5 * 1000; // 5 sec +// eslint-disable-next-line @typescript-eslint/naming-convention +export const timeoutOptions = {TTFB_TIMEOUT, RESP_TIMEOUT, REQUEST_TIMEOUT, DIAL_TIMEOUT}; + +export const MAX_VARINT_BYTES = 10; \ No newline at end of file diff --git a/packages/beacon-node/src/network/reqresp/encoders/requestDecode.ts b/packages/reqresp/src/encoders/requestDecode.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encoders/requestDecode.ts rename to packages/reqresp/src/encoders/requestDecode.ts diff --git a/packages/beacon-node/src/network/reqresp/encoders/requestEncode.ts b/packages/reqresp/src/encoders/requestEncode.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encoders/requestEncode.ts rename to packages/reqresp/src/encoders/requestEncode.ts diff --git a/packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts b/packages/reqresp/src/encoders/responseDecode.ts similarity index 98% rename from packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts rename to packages/reqresp/src/encoders/responseDecode.ts index 285008a48ad0..bd18e6161b87 100644 --- a/packages/beacon-node/src/network/reqresp/encoders/responseDecode.ts +++ b/packages/reqresp/src/encoders/responseDecode.ts @@ -1,11 +1,11 @@ import {Uint8ArrayList} from "uint8arraylist"; import {ForkName} from "@lodestar/params"; import {Type} from "@chainsafe/ssz"; -import {RespStatus} from "../../../constants/index.js"; import {BufferedSource, decodeErrorMessage} from "../utils/index.js"; import {readEncodedPayload} from "../encodingStrategies/index.js"; import {ResponseError} from "../response/index.js"; import {ContextBytesType, CONTEXT_BYTES_FORK_DIGEST_LENGTH, ContextBytesFactory, ProtocolDefinition} from "../types.js"; +import {RespStatus} from "../interface.js"; /** * Internal helper type to signal stream ended early diff --git a/packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts b/packages/reqresp/src/encoders/responseEncode.ts similarity index 98% rename from packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts rename to packages/reqresp/src/encoders/responseEncode.ts index 68058d4ded9b..8561db1c0705 100644 --- a/packages/beacon-node/src/network/reqresp/encoders/responseEncode.ts +++ b/packages/reqresp/src/encoders/responseEncode.ts @@ -1,5 +1,4 @@ import {ForkName} from "@lodestar/params"; -import {RespStatus, RpcResponseStatusError} from "../../../constants/index.js"; import {writeEncodedPayload} from "../encodingStrategies/index.js"; import {encodeErrorMessage} from "../utils/index.js"; import { @@ -10,6 +9,7 @@ import { EncodedPayload, EncodedPayloadType, } from "../types.js"; +import {RespStatus, RpcResponseStatusError} from "../interface.js"; /** * Yields byte chunks for a `` with a zero response code `` diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/index.ts b/packages/reqresp/src/encodingStrategies/index.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/index.ts rename to packages/reqresp/src/encodingStrategies/index.ts diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts similarity index 98% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts rename to packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts index c765299ad526..959de9c45371 100644 --- a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/decode.ts +++ b/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts @@ -1,11 +1,11 @@ import varint from "varint"; import {Uint8ArrayList} from "uint8arraylist"; import {Type} from "@chainsafe/ssz"; -import {MAX_VARINT_BYTES} from "../../../../constants/index.js"; import {BufferedSource} from "../../utils/index.js"; import {SnappyFramesUncompress} from "./snappyFrames/uncompress.js"; import {maxEncodedLen} from "./utils.js"; import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; +import {MAX_VARINT_BYTES} from "../../constants.js"; export type TypeRead = Pick, "minSize" | "maxSize" | "deserialize">; diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/encode.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/encode.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/encode.ts rename to packages/reqresp/src/encodingStrategies/sszSnappy/encode.ts diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/errors.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/errors.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/errors.ts rename to packages/reqresp/src/encodingStrategies/sszSnappy/errors.ts diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/index.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/index.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/index.ts rename to packages/reqresp/src/encodingStrategies/sszSnappy/index.ts diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts rename to packages/reqresp/src/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts diff --git a/packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/utils.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/utils.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/encodingStrategies/sszSnappy/utils.ts rename to packages/reqresp/src/encodingStrategies/sszSnappy/utils.ts diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/reqresp/src/handlers/beaconBlocksByRange.ts similarity index 96% rename from packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts rename to packages/reqresp/src/handlers/beaconBlocksByRange.ts index 05b201d5142f..d04c53dea64b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/reqresp/src/handlers/beaconBlocksByRange.ts @@ -1,11 +1,10 @@ import {GENESIS_SLOT, MAX_REQUEST_BLOCKS} from "@lodestar/params"; import {allForks, phase0, Slot} from "@lodestar/types"; import {fromHexString} from "@chainsafe/ssz"; -import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; -import {RespStatus} from "../../../constants/index.js"; import {ResponseError} from "../response/index.js"; import {ContextBytesType, EncodedPayload, EncodedPayloadType} from "../types.js"; +import {IBeaconChain, IBeaconDb} from "../sharedTypes.js"; +import {RespStatus} from "../interface.js"; // TODO: Unit test diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/reqresp/src/handlers/beaconBlocksByRoot.ts similarity index 87% rename from packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts rename to packages/reqresp/src/handlers/beaconBlocksByRoot.ts index 26273d56440a..84993bf9a687 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/reqresp/src/handlers/beaconBlocksByRoot.ts @@ -1,8 +1,7 @@ import {allForks, phase0, Slot} from "@lodestar/types"; -import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; -import {getSlotFromBytes} from "../../../util/multifork.js"; +import {IBeaconChain, IBeaconDb} from "../sharedTypes.js"; import {ContextBytesType, EncodedPayload, EncodedPayloadType} from "../types.js"; +import {getSlotFromBytes} from "../utils/index.js"; export async function* onBeaconBlocksByRoot( requestBody: phase0.BeaconBlocksByRootRequest, diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/reqresp/src/handlers/index.ts similarity index 95% rename from packages/beacon-node/src/network/reqresp/handlers/index.ts rename to packages/reqresp/src/handlers/index.ts index 123a894c750b..f9f9a9f3e894 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/reqresp/src/handlers/index.ts @@ -1,7 +1,6 @@ import {allForks, altair, phase0, Root} from "@lodestar/types"; -import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; import {EncodedPayload, EncodedPayloadType} from "../types.js"; +import {IBeaconChain, IBeaconDb} from "../sharedTypes.js"; import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js"; import {onBeaconBlocksByRoot} from "./beaconBlocksByRoot.js"; import {onLightClientBootstrap} from "./lightClientBootstrap.js"; diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts b/packages/reqresp/src/handlers/lightClientBootstrap.ts similarity index 77% rename from packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts rename to packages/reqresp/src/handlers/lightClientBootstrap.ts index 45bd9ed7fca3..264d421ac780 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts +++ b/packages/reqresp/src/handlers/lightClientBootstrap.ts @@ -1,8 +1,7 @@ import {altair, Root} from "@lodestar/types"; -import {IBeaconChain} from "../../../chain/index.js"; import {ResponseError} from "../response/index.js"; -import {RespStatus} from "../../../constants/index.js"; -import {LightClientServerError, LightClientServerErrorCode} from "../../../chain/errors/lightClientError.js"; +import {IBeaconChain, LightClientServerError, LightClientServerErrorCode} from "../sharedTypes.js"; +import {RespStatus} from "../interface.js"; import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientBootstrap( diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts b/packages/reqresp/src/handlers/lightClientFinalityUpdate.ts similarity index 84% rename from packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts rename to packages/reqresp/src/handlers/lightClientFinalityUpdate.ts index fdc0a29af653..773beb53986f 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts +++ b/packages/reqresp/src/handlers/lightClientFinalityUpdate.ts @@ -1,7 +1,7 @@ import {altair} from "@lodestar/types"; -import {IBeaconChain} from "../../../chain/index.js"; import {ResponseError} from "../response/index.js"; -import {RespStatus} from "../../../constants/index.js"; +import {IBeaconChain} from "../sharedTypes.js"; +import {RespStatus} from "../interface.js"; import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientFinalityUpdate( diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts b/packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts similarity index 84% rename from packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts rename to packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts index b7fcf0d285ad..a246918dea17 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts +++ b/packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts @@ -1,7 +1,7 @@ import {altair} from "@lodestar/types"; -import {IBeaconChain} from "../../../chain/index.js"; import {ResponseError} from "../response/index.js"; -import {RespStatus} from "../../../constants/index.js"; +import {IBeaconChain} from "../sharedTypes.js"; +import {RespStatus} from "../interface.js"; import {EncodedPayload, EncodedPayloadType} from "../types.js"; export async function* onLightClientOptimisticUpdate( diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts b/packages/reqresp/src/handlers/lightClientUpdatesByRange.ts similarity index 82% rename from packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts rename to packages/reqresp/src/handlers/lightClientUpdatesByRange.ts index d2b43af9df37..7699cadc435b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts +++ b/packages/reqresp/src/handlers/lightClientUpdatesByRange.ts @@ -1,10 +1,9 @@ import {altair} from "@lodestar/types"; import {MAX_REQUEST_LIGHT_CLIENT_UPDATES} from "@lodestar/params"; -import {IBeaconChain} from "../../../chain/index.js"; -import {LightClientServerError, LightClientServerErrorCode} from "../../../chain/errors/lightClientError.js"; import {ResponseError} from "../response/errors.js"; -import {RespStatus} from "../../../constants/network.js"; import {EncodedPayload, EncodedPayloadType} from "../types.js"; +import {IBeaconChain, LightClientServerError, LightClientServerErrorCode} from "../sharedTypes.js"; +import {RespStatus} from "../interface.js"; export async function* onLightClientUpdatesByRange( requestBody: altair.LightClientUpdatesByRange, diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/reqresp/src/index.ts similarity index 91% rename from packages/beacon-node/src/network/reqresp/index.ts rename to packages/reqresp/src/index.ts index 67d4bc6c5049..da92c285cc90 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/reqresp/src/index.ts @@ -1,5 +1,6 @@ export {ReqResp, IReqRespOptions} from "./reqResp.js"; export {ReqRespHandlers, getReqRespHandlers} from "./handlers/index.js"; export * from "./interface.js"; +export * from "./constants.js"; export {RequestTypedContainer} from "./types.js"; // To type-safe reqResp event listeners export {Encoding as ReqRespEncoding, Method as ReqRespMethod} from "./types.js"; // Expose enums renamed diff --git a/packages/beacon-node/src/network/reqresp/interface.ts b/packages/reqresp/src/interface.ts similarity index 63% rename from packages/beacon-node/src/network/reqresp/interface.ts rename to packages/reqresp/src/interface.ts index b8d4d2138885..e69436cddcd1 100644 --- a/packages/beacon-node/src/network/reqresp/interface.ts +++ b/packages/reqresp/src/interface.ts @@ -4,12 +4,9 @@ import {ForkName} from "@lodestar/params"; import {IBeaconConfig} from "@lodestar/config"; import {allForks, altair, phase0} from "@lodestar/types"; import {ILogger} from "@lodestar/utils"; -import {IPeerRpcScoreStore} from "../peers/index.js"; -import {MetadataController} from "../metadata.js"; -import {INetworkEventBus} from "../events.js"; -import {PeersData} from "../peers/peersData.js"; -import {IMetrics} from "../../metrics/index.js"; import {ReqRespHandlers} from "./handlers/index.js"; +import {INetworkEventBus, IPeerRpcScoreStore, MetadataController, PeersData} from "./sharedTypes.js"; +import {Metrics} from "./metrics.js"; export interface IReqResp { start(): void; @@ -39,7 +36,7 @@ export interface IReqRespModules { reqRespHandlers: ReqRespHandlers; peerRpcScores: IPeerRpcScoreStore; networkEventBus: INetworkEventBus; - metrics: IMetrics | null; + metrics: Metrics | null; } /** @@ -58,3 +55,30 @@ export interface IRateLimiter { start(): void; stop(): void; } + +// Request/Response constants +export enum RespStatus { + /** + * A normal response follows, with contents matching the expected message schema and encoding specified in the request + */ + SUCCESS = 0, + /** + * The contents of the request are semantically invalid, or the payload is malformed, + * or could not be understood. The response payload adheres to the ErrorMessage schema + */ + INVALID_REQUEST = 1, + /** + * The responder encountered an error while processing the request. The response payload adheres to the ErrorMessage schema + */ + SERVER_ERROR = 2, + /** + * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema (described below). Note: This response code is only valid as a response to BlocksByRange + */ + RESOURCE_UNAVAILABLE = 3, + /** + * Our node does not have bandwidth to serve requests due to either per-peer quota or total quota. + */ + RATE_LIMITED = 139, +} + +export type RpcResponseStatusError = Exclude; diff --git a/packages/reqresp/src/metrics.ts b/packages/reqresp/src/metrics.ts new file mode 100644 index 000000000000..5f09721744bd --- /dev/null +++ b/packages/reqresp/src/metrics.ts @@ -0,0 +1,129 @@ +type LabelsGeneric = Record; +type CollectFn = (metric: Gauge) => void; + +interface Gauge { + // Sorry for this mess, `prom-client` API choices are not great + // If the function signature was `inc(value: number, labels?: Labels)`, this would be simpler + inc(value?: number): void; + inc(labels: Labels, value?: number): void; + inc(arg1?: Labels | number, arg2?: number): void; + + dec(value?: number): void; + dec(labels: Labels, value?: number): void; + dec(arg1?: Labels | number, arg2?: number): void; + + set(value: number): void; + set(labels: Labels, value: number): void; + set(arg1?: Labels | number, arg2?: number): void; + + addCollect(collectFn: CollectFn): void; +} + +interface Histogram { + startTimer(arg1?: Labels): (labels?: Labels) => number; + + observe(value: number): void; + observe(labels: Labels, values: number): void; + observe(arg1: Labels | number, arg2?: number): void; + + reset(): void; +} + +interface AvgMinMax { + set(values: number[]): void; + set(labels: Labels, values: number[]): void; + set(arg1?: Labels | number[], arg2?: number[]): void; +} + +type GaugeConfig = { + name: string; + help: string; + labelNames?: keyof Labels extends string ? (keyof Labels)[] : undefined; +}; + +type HistogramConfig = { + name: string; + help: string; + labelNames?: (keyof Labels)[]; + buckets?: number[]; +}; + +type AvgMinMaxConfig = GaugeConfig; + +export interface MetricsRegister { + gauge(config: GaugeConfig): Gauge; + histogram(config: HistogramConfig): Histogram; + avgMinMax(config: AvgMinMaxConfig): AvgMinMax; +} + +export type Metrics = ReturnType; + +export type LodestarGitData = { + /** "0.16.0 developer/feature-1 ac99f2b5" */ + version: string; + /** "4f816b16dfde718e2d74f95f2c8292596138c248" */ + commit: string; + /** "goerli" */ + network: string; +}; + +/** + * A collection of metrics used throughout the Gossipsub behaviour. + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type +export function getMetrics(register: MetricsRegister, gitData: LodestarGitData) { + // Using function style instead of class to prevent having to re-declare all MetricsPrometheus types. + + // Track version, same as https://github.com/ChainSafe/lodestar/blob/6df28de64f12ea90b341b219229a47c8a25c9343/packages/lodestar/src/metrics/metrics/lodestar.ts#L17 + register + .gauge({ + name: "lodestar_version", + help: "Lodestar version", + labelNames: Object.keys(gitData) as (keyof LodestarGitData)[], + }) + .set(gitData, 1); + + return { + outgoingRequests: register.gauge<{method: string}>({ + name: "beacon_reqresp_outgoing_requests_total", + help: "Counts total requests done per method", + labelNames: ["method"], + }), + outgoingRequestRoundtripTime: register.histogram<{method: string}>({ + name: "beacon_reqresp_outgoing_request_roundtrip_time_seconds", + help: "Histogram of outgoing requests round-trip time", + labelNames: ["method"], + buckets: [0.1, 0.2, 0.5, 1, 5, 15, 60], + }), + outgoingErrors: register.gauge<{method: string}>({ + name: "beacon_reqresp_outgoing_requests_error_total", + help: "Counts total failed requests done per method", + labelNames: ["method"], + }), + incomingRequests: register.gauge<{method: string}>({ + name: "beacon_reqresp_incoming_requests_total", + help: "Counts total responses handled per method", + labelNames: ["method"], + }), + incomingRequestHandlerTime: register.histogram<{method: string}>({ + name: "beacon_reqresp_incoming_request_handler_time_seconds", + help: "Histogram of incoming requests internal handling time", + labelNames: ["method"], + buckets: [0.1, 0.2, 0.5, 1, 5], + }), + incomingErrors: register.gauge<{method: string}>({ + name: "beacon_reqresp_incoming_requests_error_total", + help: "Counts total failed responses handled per method", + labelNames: ["method"], + }), + dialErrors: register.gauge({ + name: "beacon_reqresp_dial_errors_total", + help: "Count total dial errors", + }), + rateLimitErrors: register.gauge<{tracker: string}>({ + name: "beacon_reqresp_rate_limiter_errors_total", + help: "Count rate limiter errors", + labelNames: ["tracker"], + }), + }; +} diff --git a/packages/beacon-node/src/network/reqresp/rateTracker.ts b/packages/reqresp/src/rateTracker.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/rateTracker.ts rename to packages/reqresp/src/rateTracker.ts diff --git a/packages/beacon-node/src/network/reqresp/reqResp.ts b/packages/reqresp/src/reqResp.ts similarity index 98% rename from packages/beacon-node/src/network/reqresp/reqResp.ts rename to packages/reqresp/src/reqResp.ts index 01927e64c21f..79bd8d593070 100644 --- a/packages/beacon-node/src/network/reqresp/reqResp.ts +++ b/packages/reqresp/src/reqResp.ts @@ -3,10 +3,7 @@ import {Type} from "@chainsafe/ssz"; import {ForkName} from "@lodestar/params"; import {allForks, altair, phase0, Root, Slot, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; -import {RespStatus, timeoutOptions} from "../../constants/index.js"; -import {MetadataController} from "../metadata.js"; -import {IPeerRpcScoreStore} from "../peers/score.js"; -import {INetworkEventBus, NetworkEvent} from "../events.js"; +import {RespStatus} from "./interface.js"; import {IReqResp, IReqRespModules, IRateLimiter} from "./interface.js"; import {ResponseError} from "./response/index.js"; import {assertSequentialBlocksInRange} from "./utils/index.js"; @@ -25,6 +22,8 @@ import {InboundRateLimiter, RateLimiterOpts} from "./response/rateLimiter.js"; import {ReqRespProtocol} from "./reqRespProtocol.js"; import {RequestError} from "./request/errors.js"; import {onOutgoingReqRespError} from "./score.js"; +import {timeoutOptions} from "./constants.js"; +import {MetadataController, IPeerRpcScoreStore, INetworkEventBus, NetworkEvent} from "./sharedTypes.js"; export type IReqRespOptions = Partial; diff --git a/packages/beacon-node/src/network/reqresp/reqRespProtocol.ts b/packages/reqresp/src/reqRespProtocol.ts similarity index 94% rename from packages/beacon-node/src/network/reqresp/reqRespProtocol.ts rename to packages/reqresp/src/reqRespProtocol.ts index f72b163ce096..43a20357d69c 100644 --- a/packages/beacon-node/src/network/reqresp/reqRespProtocol.ts +++ b/packages/reqresp/src/reqRespProtocol.ts @@ -4,15 +4,14 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {Connection, Stream} from "@libp2p/interface-connection"; import {IBeaconConfig} from "@lodestar/config"; import {ILogger} from "@lodestar/utils"; -import {timeoutOptions} from "../../constants/index.js"; -import {PeersData} from "../peers/peersData.js"; -import {IPeerRpcScoreStore} from "../peers/score.js"; -import {IMetrics} from "../../metrics/metrics.js"; import {sendRequest} from "./request/index.js"; import {handleRequest} from "./response/index.js"; import {formatProtocolID} from "./utils/index.js"; import {RequestError, RequestErrorCode} from "./request/index.js"; import {Encoding, ProtocolDefinition} from "./types.js"; +import {timeoutOptions} from "./constants.js"; +import {IPeerRpcScoreStore, PeersData} from "./sharedTypes.js"; +import {Metrics} from "./metrics.js"; export type IReqRespOptions = Partial; type ProtocolID = string; @@ -23,7 +22,7 @@ export interface ReqRespProtocolModules { peersData: PeersData; logger: ILogger; peerRpcScores: IPeerRpcScoreStore; - metrics: IMetrics | null; + metrics: Metrics | null; } /** @@ -40,7 +39,7 @@ export class ReqRespProtocol { private options?: IReqRespOptions; private reqCount = 0; private respCount = 0; - private metrics: IMetrics["reqResp"] | null; + private metrics: Metrics | null; /** `${protocolPrefix}/${method}/${version}/${encoding}` */ private readonly supportedProtocols = new Map(); @@ -49,7 +48,7 @@ export class ReqRespProtocol { this.peersData = modules.peersData; this.logger = modules.logger; this.options = options; - this.metrics = modules.metrics?.reqResp ?? null; + this.metrics = modules.metrics ?? null; } registerProtocol(protocol: ProtocolDefinition): void { diff --git a/packages/beacon-node/src/network/reqresp/request/collectResponses.ts b/packages/reqresp/src/request/collectResponses.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/request/collectResponses.ts rename to packages/reqresp/src/request/collectResponses.ts diff --git a/packages/beacon-node/src/network/reqresp/request/errors.ts b/packages/reqresp/src/request/errors.ts similarity index 98% rename from packages/beacon-node/src/network/reqresp/request/errors.ts rename to packages/reqresp/src/request/errors.ts index 6c3b7019154b..ce9b2fcacdb6 100644 --- a/packages/beacon-node/src/network/reqresp/request/errors.ts +++ b/packages/reqresp/src/request/errors.ts @@ -1,7 +1,7 @@ import {LodestarError} from "@lodestar/utils"; -import {RespStatus, RpcResponseStatusError} from "../../../constants/index.js"; import {Method, Encoding} from "../types.js"; import {ResponseError} from "../response/index.js"; +import {RespStatus, RpcResponseStatusError} from "../interface.js"; export enum RequestErrorCode { // Declaring specific values of RpcResponseStatusError for error clarity downstream diff --git a/packages/beacon-node/src/network/reqresp/request/index.ts b/packages/reqresp/src/request/index.ts similarity index 97% rename from packages/beacon-node/src/network/reqresp/request/index.ts rename to packages/reqresp/src/request/index.ts index 569405162077..7562d282079d 100644 --- a/packages/beacon-node/src/network/reqresp/request/index.ts +++ b/packages/reqresp/src/request/index.ts @@ -3,14 +3,12 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {Libp2p} from "libp2p"; import {Uint8ArrayList} from "uint8arraylist"; import {ErrorAborted, ILogger, withTimeout, TimeoutError} from "@lodestar/utils"; -import {timeoutOptions} from "../../../constants/index.js"; -import {prettyPrintPeerId} from "../../util.js"; -import {abortableSource} from "../../../util/abortableSource.js"; import {ProtocolDefinition} from "../types.js"; -import {formatProtocolID} from "../utils/index.js"; +import {formatProtocolID, prettyPrintPeerId, abortableSource} from "../utils/index.js"; import {ResponseError} from "../response/index.js"; import {requestEncode} from "../encoders/requestEncode.js"; import {responseDecode} from "../encoders/responseDecode.js"; +import {timeoutOptions} from "../constants.js"; import {collectResponses} from "./collectResponses.js"; import { RequestError, diff --git a/packages/beacon-node/src/network/reqresp/response/errors.ts b/packages/reqresp/src/response/errors.ts similarity index 92% rename from packages/beacon-node/src/network/reqresp/response/errors.ts rename to packages/reqresp/src/response/errors.ts index b6af866959bc..44c311ae1ce6 100644 --- a/packages/beacon-node/src/network/reqresp/response/errors.ts +++ b/packages/reqresp/src/response/errors.ts @@ -1,5 +1,5 @@ import {LodestarError} from "@lodestar/utils"; -import {RespStatus, RpcResponseStatusError} from "../../../constants/index.js"; +import {RespStatus, RpcResponseStatusError} from "../interface.js"; type RpcResponseStatusNotSuccess = Exclude; diff --git a/packages/beacon-node/src/network/reqresp/response/index.ts b/packages/reqresp/src/response/index.ts similarity index 96% rename from packages/beacon-node/src/network/reqresp/response/index.ts rename to packages/reqresp/src/response/index.ts index c623f7b9568a..da6bbf1e4ef6 100644 --- a/packages/beacon-node/src/network/reqresp/response/index.ts +++ b/packages/reqresp/src/response/index.ts @@ -3,11 +3,12 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {Stream} from "@libp2p/interface-connection"; import {Uint8ArrayList} from "uint8arraylist"; import {ILogger, TimeoutError, withTimeout} from "@lodestar/utils"; -import {REQUEST_TIMEOUT, RespStatus} from "../../../constants/index.js"; -import {prettyPrintPeerId} from "../../util.js"; +import {REQUEST_TIMEOUT} from "../constants.js"; +import {prettyPrintPeerId} from "../utils/index.js"; import {ProtocolDefinition} from "../types.js"; import {requestDecode} from "../encoders/requestDecode.js"; import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js"; +import {RespStatus} from "../interface.js"; import {ResponseError} from "./errors.js"; export {ResponseError}; diff --git a/packages/beacon-node/src/network/reqresp/response/rateLimiter.ts b/packages/reqresp/src/response/rateLimiter.ts similarity index 92% rename from packages/beacon-node/src/network/reqresp/response/rateLimiter.ts rename to packages/reqresp/src/response/rateLimiter.ts index 8b5d9cd12638..7faf03461658 100644 --- a/packages/beacon-node/src/network/reqresp/response/rateLimiter.ts +++ b/packages/reqresp/src/response/rateLimiter.ts @@ -1,14 +1,14 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ILogger, MapDef} from "@lodestar/utils"; -import {IMetrics} from "../../../metrics/index.js"; -import {IPeerRpcScoreStore, PeerAction} from "../../peers/score.js"; import {IRateLimiter} from "../interface.js"; +import {Metrics} from "../metrics.js"; import {RateTracker} from "../rateTracker.js"; +import {IPeerRpcScoreStore, PeerAction} from "../sharedTypes.js"; interface IRateLimiterModules { logger: ILogger; peerRpcScores: IPeerRpcScoreStore; - metrics: IMetrics | null; + metrics: Metrics | null; } /** @@ -52,7 +52,7 @@ export const defaultRateLimiterOpts = { export class InboundRateLimiter implements IRateLimiter { private readonly logger: ILogger; private readonly peerRpcScores: IPeerRpcScoreStore; - private readonly metrics: IMetrics | null; + private readonly metrics: Metrics | null; private requestCountTrackersByPeer: MapDef; /** * This rate tracker is specific to lodestar, we don't want to serve too many blocks for peers at the @@ -108,7 +108,7 @@ export class InboundRateLimiter implements IRateLimiter { }); this.peerRpcScores.applyAction(peerId, PeerAction.Fatal, "RateLimit"); if (this.metrics) { - this.metrics.reqResp.rateLimitErrors.inc({tracker: "requestCountPeerTracker"}); + this.metrics.rateLimitErrors.inc({tracker: "requestCountPeerTracker"}); } return false; } @@ -131,14 +131,14 @@ export class InboundRateLimiter implements IRateLimiter { }); this.peerRpcScores.applyAction(peerId, PeerAction.Fatal, "RateLimit"); if (this.metrics) { - this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); + this.metrics.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); } return false; } if (this.blockCountTotalTracker.requestObjects(numBlock) === 0) { if (this.metrics) { - this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountTotalTracker"}); + this.metrics.rateLimitErrors.inc({tracker: "blockCountTotalTracker"}); } // don't apply penalty return false; diff --git a/packages/beacon-node/src/network/reqresp/score.ts b/packages/reqresp/src/score.ts similarity index 97% rename from packages/beacon-node/src/network/reqresp/score.ts rename to packages/reqresp/src/score.ts index fd0dca4c5e6d..9a121f9430fa 100644 --- a/packages/beacon-node/src/network/reqresp/score.ts +++ b/packages/reqresp/src/score.ts @@ -1,6 +1,6 @@ -import {PeerAction} from "../peers/score.js"; import {Method} from "./types.js"; import {RequestError, RequestErrorCode} from "./request/index.js"; +import {PeerAction} from "./sharedTypes.js"; /** * libp2p-ts does not include types for the error codes. diff --git a/packages/reqresp/src/sharedTypes.ts b/packages/reqresp/src/sharedTypes.ts new file mode 100644 index 000000000000..b0e1119c4aa2 --- /dev/null +++ b/packages/reqresp/src/sharedTypes.ts @@ -0,0 +1,149 @@ +import {EventEmitter} from "events"; +import {PeerId} from "@libp2p/interface-peer-id"; +import StrictEventEmitter from "strict-event-emitter-types"; +import {ENR} from "@chainsafe/discv5"; +import {BitArray} from "@chainsafe/ssz"; +import {ForkName} from "@lodestar/params"; +import {allForks, altair, Epoch, phase0, Root, RootHex, Slot} from "@lodestar/types"; +import {LodestarError} from "@lodestar/utils"; +import {Encoding, RequestTypedContainer} from "./types.js"; + +// These interfaces are shared among beacon-node package. +export enum ScoreState { + /** We are content with the peers performance. We permit connections and messages. */ + Healthy = "Healthy", + /** The peer should be disconnected. We allow re-connections if the peer is persistent */ + Disconnected = "Disconnected", + /** The peer is banned. We disallow new connections until it's score has decayed into a tolerable threshold */ + Banned = "Banned", +} + +type PeerIdStr = string; + +export enum PeerAction { + /** Immediately ban peer */ + Fatal = "Fatal", + /** + * Not malicious action, but it must not be tolerated + * ~5 occurrences will get the peer banned + */ + LowToleranceError = "LowToleranceError", + /** + * Negative action that can be tolerated only sometimes + * ~10 occurrences will get the peer banned + */ + MidToleranceError = "MidToleranceError", + /** + * Some error that can be tolerated multiple times + * ~50 occurrences will get the peer banned + */ + HighToleranceError = "HighToleranceError", +} + +export interface IPeerRpcScoreStore { + getScore(peer: PeerId): number; + getScoreState(peer: PeerId): ScoreState; + applyAction(peer: PeerId, action: PeerAction, actionName: string): void; + update(): void; + updateGossipsubScore(peerId: PeerIdStr, newScore: number, ignore: boolean): void; +} + +export enum NetworkEvent { + /** A relevant peer has connected or has been re-STATUS'd */ + peerConnected = "peer-manager.peer-connected", + peerDisconnected = "peer-manager.peer-disconnected", + gossipStart = "gossip.start", + gossipStop = "gossip.stop", + gossipHeartbeat = "gossipsub.heartbeat", + reqRespRequest = "req-resp.request", + unknownBlockParent = "unknownBlockParent", +} + +export type NetworkEvents = { + [NetworkEvent.peerConnected]: (peer: PeerId, status: phase0.Status) => void; + [NetworkEvent.peerDisconnected]: (peer: PeerId) => void; + [NetworkEvent.reqRespRequest]: (request: RequestTypedContainer, peer: PeerId) => void; + [NetworkEvent.unknownBlockParent]: (signedBlock: allForks.SignedBeaconBlock, peerIdStr: string) => void; +}; + +export type INetworkEventBus = StrictEventEmitter; + +export enum RelevantPeerStatus { + Unknown = "unknown", + relevant = "relevant", + irrelevant = "irrelevant", +} + +export type PeerData = { + lastReceivedMsgUnixTsMs: number; + lastStatusUnixTsMs: number; + connectedUnixTsMs: number; + relevantStatus: RelevantPeerStatus; + direction: "inbound" | "outbound"; + peerId: PeerId; + metadata: altair.Metadata | null; + agentVersion: string | null; + agentClient: ClientKind | null; + encodingPreference: Encoding | null; +}; + +export enum ClientKind { + Lighthouse = "Lighthouse", + Nimbus = "Nimbus", + Teku = "Teku", + Prysm = "Prysm", + Lodestar = "Lodestar", + Unknown = "Unknown", +} + +export interface PeersData { + getAgentVersion(peerIdStr: string): string; + getPeerKind(peerIdStr: string): ClientKind; + getEncodingPreference(peerIdStr: string): Encoding | null; + setEncodingPreference(peerIdStr: string, encoding: Encoding): void; +} + +export interface MetadataController { + seqNumber: bigint; + syncnets: BitArray; + attnets: BitArray; + json: altair.Metadata; + start(enr: ENR | undefined, currentFork: ForkName): void; + updateEth2Field(epoch: Epoch): void; +} + +export interface IBeaconChain { + getStatus(): phase0.Status; + + readonly lightClientServer: { + getUpdate(period: number): Promise; + getOptimisticUpdate(): altair.LightClientOptimisticUpdate | null; + getFinalityUpdate(): altair.LightClientFinalityUpdate | null; + getBootstrap(blockRoot: Uint8Array): Promise; + }; + readonly forkChoice: { + // For our use case we don't need the full block in this package + getBlock(blockRoot: Root): Uint8Array | null; + getHeadRoot(): RootHex; + iterateAncestorBlocks(blockRoot: RootHex): IterableIterator<{slot: Slot; blockRoot: RootHex}>; + }; +} + +export enum LightClientServerErrorCode { + RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE", +} + +export type LightClientServerErrorType = {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}; + +export class LightClientServerError extends LodestarError {} + +export interface IBeaconDb { + readonly block: { + getBinary: (blockRoot: Uint8Array) => Promise; + }; + readonly blockArchive: { + getBinaryEntryByRoot(blockRoot: Uint8Array): Promise<{key: Slot; value: Uint8Array | null}>; + decodeKey(data: Uint8Array): number; + binaryEntriesStream(opts?: {gte: number; lt: number}): AsyncIterable<{key: Uint8Array; value: Uint8Array}>; + }; +} diff --git a/packages/beacon-node/src/network/reqresp/types.ts b/packages/reqresp/src/types.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/types.ts rename to packages/reqresp/src/types.ts diff --git a/packages/beacon-node/src/util/abortableSource.ts b/packages/reqresp/src/utils/abortableSource.ts similarity index 100% rename from packages/beacon-node/src/util/abortableSource.ts rename to packages/reqresp/src/utils/abortableSource.ts diff --git a/packages/beacon-node/src/network/reqresp/utils/assertSequentialBlocksInRange.ts b/packages/reqresp/src/utils/assertSequentialBlocksInRange.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/utils/assertSequentialBlocksInRange.ts rename to packages/reqresp/src/utils/assertSequentialBlocksInRange.ts diff --git a/packages/beacon-node/src/network/reqresp/utils/bufferedSource.ts b/packages/reqresp/src/utils/bufferedSource.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/utils/bufferedSource.ts rename to packages/reqresp/src/utils/bufferedSource.ts diff --git a/packages/beacon-node/src/network/reqresp/utils/errorMessage.ts b/packages/reqresp/src/utils/errorMessage.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/utils/errorMessage.ts rename to packages/reqresp/src/utils/errorMessage.ts diff --git a/packages/beacon-node/src/network/reqresp/utils/index.ts b/packages/reqresp/src/utils/index.ts similarity index 65% rename from packages/beacon-node/src/network/reqresp/utils/index.ts rename to packages/reqresp/src/utils/index.ts index b5799cd88d46..daabe0458abc 100644 --- a/packages/beacon-node/src/network/reqresp/utils/index.ts +++ b/packages/reqresp/src/utils/index.ts @@ -3,3 +3,6 @@ export * from "./bufferedSource.js"; export * from "./errorMessage.js"; export * from "./onChunk.js"; export * from "./protocolId.js"; +export * from "./peerId.js"; +export * from "./abortableSource.js"; +export * from "./multifork.js"; diff --git a/packages/reqresp/src/utils/multifork.ts b/packages/reqresp/src/utils/multifork.ts new file mode 100644 index 000000000000..dccec9a90821 --- /dev/null +++ b/packages/reqresp/src/utils/multifork.ts @@ -0,0 +1,9 @@ +import {Slot} from "@lodestar/types"; +import {bytesToInt} from "@lodestar/utils"; + +const SLOT_BYTES_POSITION_IN_BLOCK = 100; +const SLOT_BYTE_COUNT = 8; + +export function getSlotFromBytes(bytes: Buffer | Uint8Array): Slot { + return bytesToInt(bytes.subarray(SLOT_BYTES_POSITION_IN_BLOCK, SLOT_BYTES_POSITION_IN_BLOCK + SLOT_BYTE_COUNT)); +} diff --git a/packages/beacon-node/src/network/reqresp/utils/onChunk.ts b/packages/reqresp/src/utils/onChunk.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/utils/onChunk.ts rename to packages/reqresp/src/utils/onChunk.ts diff --git a/packages/reqresp/src/utils/peerId.ts b/packages/reqresp/src/utils/peerId.ts new file mode 100644 index 000000000000..db37b718c94a --- /dev/null +++ b/packages/reqresp/src/utils/peerId.ts @@ -0,0 +1,6 @@ +import {PeerId} from "@libp2p/interface-peer-id"; + +export function prettyPrintPeerId(peerId: PeerId): string { + const id = peerId.toString(); + return `${id.substr(0, 2)}...${id.substr(id.length - 6, id.length)}`; +} diff --git a/packages/beacon-node/src/network/reqresp/utils/protocolId.ts b/packages/reqresp/src/utils/protocolId.ts similarity index 100% rename from packages/beacon-node/src/network/reqresp/utils/protocolId.ts rename to packages/reqresp/src/utils/protocolId.ts diff --git a/packages/reqresp/tsconfig.build.json b/packages/reqresp/tsconfig.build.json index 92235557ba5d..b46adfa48cb7 100644 --- a/packages/reqresp/tsconfig.build.json +++ b/packages/reqresp/tsconfig.build.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.build.json", "include": ["src"], "compilerOptions": { - "outDir": "lib" + "outDir": "lib", + "typeRoots": ["../../node_modules/@types", "./node_modules/@types", "../../types"] } } diff --git a/packages/reqresp/tsconfig.json b/packages/reqresp/tsconfig.json index b29a7b46c4b1..ed997ec031fa 100644 --- a/packages/reqresp/tsconfig.json +++ b/packages/reqresp/tsconfig.json @@ -1,4 +1,5 @@ { "extends": "../../tsconfig.json", - "compilerOptions": {} + "exclude": ["../../node_modules/it-pipe"], + "typeRoots": ["../../node_modules/@types", "../../types"] } From 3f8b18751c3538c1ae6cb37342a1ba55fa2fcc68 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 17 Nov 2022 00:58:57 +0100 Subject: [PATCH 04/23] Extract protocol messages from implementation --- packages/reqresp/package.json | 11 +- packages/reqresp/src/ReqResp.ts | 174 ++++++++ ...{reqRespProtocol.ts => ReqRespProtocol.ts} | 40 +- packages/reqresp/src/constants.ts | 2 +- .../src/handlers/beaconBlocksByRange.ts | 152 ------- .../src/handlers/beaconBlocksByRoot.ts | 40 -- packages/reqresp/src/handlers/index.ts | 55 --- .../src/handlers/lightClientBootstrap.ts | 23 -- .../src/handlers/lightClientFinalityUpdate.ts | 19 - .../handlers/lightClientOptimisticUpdate.ts | 19 - .../src/handlers/lightClientUpdatesByRange.ts | 27 -- packages/reqresp/src/index.ts | 9 +- packages/reqresp/src/interface.ts | 22 +- packages/reqresp/src/messages/index.ts | 34 ++ packages/reqresp/src/messages/utils.ts | 14 + .../src/messages/v1/BeaconBlocksByRange.ts | 28 ++ .../src/messages/v1/BeaconBlocksByRoot.ts | 29 ++ packages/reqresp/src/messages/v1/Goodbye.ts | 28 ++ .../src/messages/v1/LightClientBootstrap.ts | 24 ++ .../messages/v1/LightClientFinalityUpdate.ts | 22 + .../v1/LightClientOptimisticUpdate.ts | 22 + .../messages/v1/LightClientUpdatesByRange.ts | 23 ++ packages/reqresp/src/messages/v1/Metadata.ts | 27 ++ packages/reqresp/src/messages/v1/Ping.ts | 28 ++ packages/reqresp/src/messages/v1/Status.ts | 20 + .../src/messages/v2/BeaconBlocksByRange.ts | 32 ++ .../src/messages/v2/BeaconBlocksByRoot.ts | 33 ++ packages/reqresp/src/messages/v2/Metadata.ts | 27 ++ .../RateLimiter.ts} | 51 +-- .../RateTracker.ts} | 0 packages/reqresp/src/rate_limiter/index.ts | 2 + packages/reqresp/src/reqResp.ts | 381 ------------------ packages/reqresp/src/response/index.ts | 6 +- packages/reqresp/src/sharedTypes.ts | 39 +- packages/reqresp/src/types.ts | 28 +- 35 files changed, 686 insertions(+), 805 deletions(-) create mode 100644 packages/reqresp/src/ReqResp.ts rename packages/reqresp/src/{reqRespProtocol.ts => ReqRespProtocol.ts} (84%) delete mode 100644 packages/reqresp/src/handlers/beaconBlocksByRange.ts delete mode 100644 packages/reqresp/src/handlers/beaconBlocksByRoot.ts delete mode 100644 packages/reqresp/src/handlers/index.ts delete mode 100644 packages/reqresp/src/handlers/lightClientBootstrap.ts delete mode 100644 packages/reqresp/src/handlers/lightClientFinalityUpdate.ts delete mode 100644 packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts delete mode 100644 packages/reqresp/src/handlers/lightClientUpdatesByRange.ts create mode 100644 packages/reqresp/src/messages/index.ts create mode 100644 packages/reqresp/src/messages/utils.ts create mode 100644 packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts create mode 100644 packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts create mode 100644 packages/reqresp/src/messages/v1/Goodbye.ts create mode 100644 packages/reqresp/src/messages/v1/LightClientBootstrap.ts create mode 100644 packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts create mode 100644 packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts create mode 100644 packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts create mode 100644 packages/reqresp/src/messages/v1/Metadata.ts create mode 100644 packages/reqresp/src/messages/v1/Ping.ts create mode 100644 packages/reqresp/src/messages/v1/Status.ts create mode 100644 packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts create mode 100644 packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts create mode 100644 packages/reqresp/src/messages/v2/Metadata.ts rename packages/reqresp/src/{response/rateLimiter.ts => rate_limiter/RateLimiter.ts} (80%) rename packages/reqresp/src/{rateTracker.ts => rate_limiter/RateTracker.ts} (100%) create mode 100644 packages/reqresp/src/rate_limiter/index.ts delete mode 100644 packages/reqresp/src/reqResp.ts diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index 54080588c140..e67b2a0b0d08 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -11,11 +11,20 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.2.1", + "version": "0.1.0", "type": "module", "exports": { ".": { "import": "./lib/index.js" + }, + "./messages": { + "import": "./lib/messages/index.js" + }, + "./rate_limiter": { + "import": "./lib/rate_limiter/index.js" + }, + "./utils": { + "import": "./lib/utils/index.js" } }, "typesVersions": { diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts new file mode 100644 index 000000000000..cbd84f283c43 --- /dev/null +++ b/packages/reqresp/src/ReqResp.ts @@ -0,0 +1,174 @@ +import {PeerId} from "@libp2p/interface-peer-id"; +import {ForkName} from "@lodestar/params"; +import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; +import {timeoutOptions} from "./constants.js"; +import {IReqResp, RateLimiter, ReqRespModules, RespStatus} from "./interface.js"; +import {ReqRespProtocol} from "./ReqRespProtocol.js"; +import {RequestError} from "./request/errors.js"; +import {ResponseError} from "./response/errors.js"; +import {onOutgoingReqRespError} from "./score.js"; +import {MetadataController, NetworkEvent} from "./sharedTypes.js"; +import {Method, ReqRespOptions, RequestTypedContainer, Version} from "./types.js"; +import {assertSequentialBlocksInRange} from "./utils/index.js"; + +/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ +export type ReqRespBlockResponse = { + /** Deserialized data of allForks.SignedBeaconBlock */ + bytes: Uint8Array; + slot: Slot; +}; + +/** + * Implementation of Ethereum Consensus p2p Req/Resp domain. + * For the spec that this code is based on, see: + * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain + * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain + */ +export class ReqResp extends ReqRespProtocol implements IReqResp { + private metadataController: MetadataController; + private inboundRateLimiter: RateLimiter; + + private constructor(modules: ReqRespModules, options: ReqRespOptions) { + super(modules, options); + this.metadataController = modules.metadata; + this.inboundRateLimiter = modules.inboundRateLimiter; + } + + static withDefaults(modules: ReqRespModules, options?: Partial): IReqResp { + const optionsWithDefaults = { + ...timeoutOptions, + ...{ + onIncomingRequest: (modules: ReqRespModules, peerId: PeerId, method: Method) => { + if (method !== Method.Goodbye && !modules.inboundRateLimiter.allowRequest(peerId)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + }, + onOutgoingReqRespError: ( + modules: ReqRespModules, + peerId: PeerId, + method: Method, + error: RequestError + ): void => { + const peerAction = onOutgoingReqRespError(error, method); + if (peerAction !== null) { + modules.peerRpcScores.applyAction(peerId, peerAction, error.type.code); + } + }, + onIncomingRequestBody: (modules: ReqRespModules, req: RequestTypedContainer, peerId: PeerId): void => { + // Allow onRequest to return and close the stream + // For Goodbye there may be a race condition where the listener of `receivedGoodbye` + // disconnects in the same syncronous call, preventing the stream from ending cleanly + setTimeout(() => modules.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); + }, + }, + ...options, + }; + + return new ReqResp(modules, optionsWithDefaults); + } + + async start(): Promise { + await super.start(); + this.inboundRateLimiter.start(); + } + + async stop(): Promise { + await super.stop(); + this.inboundRateLimiter.stop(); + } + + pruneOnPeerDisconnect(peerId: PeerId): void { + this.inboundRateLimiter.prune(peerId); + } + + async status(peerId: PeerId, request: phase0.Status): Promise { + return await this.sendRequest(peerId, Method.Status, [Version.V1], request); + } + + async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { + await this.sendRequest(peerId, Method.Goodbye, [Version.V1], request); + } + + async ping(peerId: PeerId): Promise { + return await this.sendRequest( + peerId, + Method.Ping, + [Version.V1], + this.metadataController.seqNumber + ); + } + + async metadata(peerId: PeerId, fork?: ForkName): Promise { + // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version + const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; + return await this.sendRequest(peerId, Method.Metadata, versions, null); + } + + async beaconBlocksByRange( + peerId: PeerId, + request: phase0.BeaconBlocksByRangeRequest + ): Promise { + const blocks = await this.sendRequest( + peerId, + Method.BeaconBlocksByRange, + [Version.V2, Version.V1], // Prioritize V2 + request, + request.count + ); + assertSequentialBlocksInRange(blocks, request); + return blocks; + } + + async beaconBlocksByRoot( + peerId: PeerId, + request: phase0.BeaconBlocksByRootRequest + ): Promise { + return await this.sendRequest( + peerId, + Method.BeaconBlocksByRoot, + [Version.V2, Version.V1], // Prioritize V2 + request, + request.length + ); + } + + async lightClientBootstrap(peerId: PeerId, request: Root): Promise { + return await this.sendRequest( + peerId, + Method.LightClientBootstrap, + [Version.V1], + request + ); + } + + async lightClientOptimisticUpdate(peerId: PeerId): Promise { + return await this.sendRequest( + peerId, + Method.LightClientOptimisticUpdate, + [Version.V1], + null + ); + } + + async lightClientFinalityUpdate(peerId: PeerId): Promise { + return await this.sendRequest( + peerId, + Method.LightClientFinalityUpdate, + [Version.V1], + null + ); + } + + async lightClientUpdate( + peerId: PeerId, + request: altair.LightClientUpdatesByRange + ): Promise { + return await this.sendRequest( + peerId, + Method.LightClientUpdatesByRange, + [Version.V1], + request, + request.count + ); + } +} diff --git a/packages/reqresp/src/reqRespProtocol.ts b/packages/reqresp/src/ReqRespProtocol.ts similarity index 84% rename from packages/reqresp/src/reqRespProtocol.ts rename to packages/reqresp/src/ReqRespProtocol.ts index 43a20357d69c..67ffacca1a02 100644 --- a/packages/reqresp/src/reqRespProtocol.ts +++ b/packages/reqresp/src/ReqRespProtocol.ts @@ -1,19 +1,17 @@ import {setMaxListeners} from "node:events"; -import {Libp2p} from "libp2p"; -import {PeerId} from "@libp2p/interface-peer-id"; import {Connection, Stream} from "@libp2p/interface-connection"; +import {PeerId} from "@libp2p/interface-peer-id"; +import {Libp2p} from "libp2p"; import {IBeaconConfig} from "@lodestar/config"; import {ILogger} from "@lodestar/utils"; -import {sendRequest} from "./request/index.js"; +import {Metrics} from "./metrics.js"; +import {RequestError, RequestErrorCode, sendRequest} from "./request/index.js"; import {handleRequest} from "./response/index.js"; +import {IPeerRpcScoreStore, MetadataController, PeersData} from "./sharedTypes.js"; +import {Encoding, ProtocolDefinition, ReqRespOptions} from "./types.js"; import {formatProtocolID} from "./utils/index.js"; -import {RequestError, RequestErrorCode} from "./request/index.js"; -import {Encoding, ProtocolDefinition} from "./types.js"; -import {timeoutOptions} from "./constants.js"; -import {IPeerRpcScoreStore, PeersData} from "./sharedTypes.js"; -import {Metrics} from "./metrics.js"; +import {RateLimiter, ReqRespHandlerContext} from "./interface.js"; -export type IReqRespOptions = Partial; type ProtocolID = string; export interface ReqRespProtocolModules { @@ -22,6 +20,8 @@ export interface ReqRespProtocolModules { peersData: PeersData; logger: ILogger; peerRpcScores: IPeerRpcScoreStore; + inboundRateLimiter: RateLimiter; + metadata: MetadataController; metrics: Metrics | null; } @@ -36,19 +36,32 @@ export class ReqRespProtocol { private readonly peersData: PeersData; private logger: ILogger; private controller = new AbortController(); - private options?: IReqRespOptions; + private options: ReqRespOptions; private reqCount = 0; private respCount = 0; private metrics: Metrics | null; /** `${protocolPrefix}/${method}/${version}/${encoding}` */ private readonly supportedProtocols = new Map(); + private readonly modules: ReqRespProtocolModules; - constructor(modules: ReqRespProtocolModules, options: IReqRespOptions) { + constructor(modules: ReqRespProtocolModules, options: ReqRespOptions) { + this.modules = modules; + this.options = options; this.libp2p = modules.libp2p; this.peersData = modules.peersData; this.logger = modules.logger; - this.options = options; - this.metrics = modules.metrics ?? null; + this.metrics = modules.metrics; + } + + getContext(): ReqRespHandlerContext { + return { + modules: this.modules, + eventsHandlers: { + onIncomingRequest: this.options.onIncomingRequest, + onIncomingRequestBody: this.options.onIncomingRequestBody, + onOutgoingReqRespError: this.options.onOutgoingReqRespError, + }, + }; } registerProtocol(protocol: ProtocolDefinition): void { @@ -143,6 +156,7 @@ export class ReqRespProtocol { try { await handleRequest({ + context: this.getContext(), logger: this.logger, stream, peerId, diff --git a/packages/reqresp/src/constants.ts b/packages/reqresp/src/constants.ts index 762d164359ab..603671c6d297 100644 --- a/packages/reqresp/src/constants.ts +++ b/packages/reqresp/src/constants.ts @@ -9,4 +9,4 @@ export const DIAL_TIMEOUT = 5 * 1000; // 5 sec // eslint-disable-next-line @typescript-eslint/naming-convention export const timeoutOptions = {TTFB_TIMEOUT, RESP_TIMEOUT, REQUEST_TIMEOUT, DIAL_TIMEOUT}; -export const MAX_VARINT_BYTES = 10; \ No newline at end of file +export const MAX_VARINT_BYTES = 10; diff --git a/packages/reqresp/src/handlers/beaconBlocksByRange.ts b/packages/reqresp/src/handlers/beaconBlocksByRange.ts deleted file mode 100644 index d04c53dea64b..000000000000 --- a/packages/reqresp/src/handlers/beaconBlocksByRange.ts +++ /dev/null @@ -1,152 +0,0 @@ -import {GENESIS_SLOT, MAX_REQUEST_BLOCKS} from "@lodestar/params"; -import {allForks, phase0, Slot} from "@lodestar/types"; -import {fromHexString} from "@chainsafe/ssz"; -import {ResponseError} from "../response/index.js"; -import {ContextBytesType, EncodedPayload, EncodedPayloadType} from "../types.js"; -import {IBeaconChain, IBeaconDb} from "../sharedTypes.js"; -import {RespStatus} from "../interface.js"; - -// TODO: Unit test - -export async function* onBeaconBlocksByRange( - request: phase0.BeaconBlocksByRangeRequest, - chain: IBeaconChain, - db: IBeaconDb -): AsyncIterable> { - const {startSlot, count} = validateBeaconBlocksByRangeRequest(request); - const lt = startSlot + count; - - // step < 1 was validated above - const archivedBlocksStream = getFinalizedBlocksByRange(startSlot, lt, db); - - // Inject recent blocks, not in the finalized cold DB - - let totalBlock = 0; - let slot = -1; - for await (const block of archivedBlocksStream) { - totalBlock++; - slot = block.slot; - yield { - type: EncodedPayloadType.bytes, - bytes: block.bytes, - contextBytes: { - type: ContextBytesType.ForkDigest, - forkSlot: block.slot, - }, - }; - } - - slot = slot === -1 ? request.startSlot : slot + request.step; - const upperSlot = request.startSlot + request.count * request.step; - const slots = [] as number[]; - while (slot < upperSlot) { - slots.push(slot); - slot += request.step; - } - - const unfinalizedBlocks = await getUnfinalizedBlocksAtSlots(slots, {chain, db}); - for (const block of unfinalizedBlocks) { - if (block !== undefined) { - totalBlock++; - yield { - type: EncodedPayloadType.bytes, - bytes: block.bytes, - contextBytes: { - type: ContextBytesType.ForkDigest, - forkSlot: block.slot, - }, - }; - } - } - if (totalBlock === 0) { - throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No block found"); - } -} - -async function* getFinalizedBlocksByRange( - gte: number, - lt: number, - db: IBeaconDb -): AsyncIterable<{slot: Slot; bytes: Uint8Array}> { - const binaryEntriesStream = db.blockArchive.binaryEntriesStream({ - gte, - lt, - }); - for await (const {key, value} of binaryEntriesStream) { - yield { - slot: db.blockArchive.decodeKey(key), - bytes: value, - }; - } -} - -/** Returned blocks have the same ordering as `slots` */ -async function getUnfinalizedBlocksAtSlots( - slots: Slot[], - {chain, db}: {chain: IBeaconChain; db: IBeaconDb} -): Promise<{slot: Slot; bytes: Uint8Array}[]> { - if (slots.length === 0) { - return []; - } - - const slotsSet = new Set(slots); - const minSlot = Math.min(...slots); // Slots must have length > 0 - const blockRootsPerSlot = new Map>(); - - // these blocks are on the same chain to head - for (const block of chain.forkChoice.iterateAncestorBlocks(chain.forkChoice.getHeadRoot())) { - if (block.slot < minSlot) { - break; - } else if (slotsSet.has(block.slot)) { - blockRootsPerSlot.set(block.slot, db.block.getBinary(fromHexString(block.blockRoot))); - } - } - - const unfinalizedBlocksOrNull = await Promise.all(slots.map((slot) => blockRootsPerSlot.get(slot))); - - const unfinalizedBlocks: {slot: Slot; bytes: Uint8Array}[] = []; - - for (let i = 0; i < unfinalizedBlocksOrNull.length; i++) { - const block = unfinalizedBlocksOrNull[i]; - if (block) { - unfinalizedBlocks.push({ - slot: slots[i], - bytes: block, - }); - } - } - - return unfinalizedBlocks; -} - -function validateBeaconBlocksByRangeRequest( - request: phase0.BeaconBlocksByRangeRequest -): phase0.BeaconBlocksByRangeRequest { - const {startSlot, step} = request; - let {count} = request; - if (step < 1) { - throw new ResponseError(RespStatus.INVALID_REQUEST, "step < 1"); - } - if (count < 1) { - throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1"); - } - // TODO: validate against MIN_EPOCHS_FOR_BLOCK_REQUESTS - if (startSlot < GENESIS_SLOT) { - throw new ResponseError(RespStatus.INVALID_REQUEST, "startSlot < genesis"); - } - - if (step > 1) { - // step > 1 is deprecated, see https://github.com/ethereum/consensus-specs/pull/2856 - count = 1; - } - - if (count > MAX_REQUEST_BLOCKS) { - count = MAX_REQUEST_BLOCKS; - } - - return { - startSlot, - step, - count, - }; -} diff --git a/packages/reqresp/src/handlers/beaconBlocksByRoot.ts b/packages/reqresp/src/handlers/beaconBlocksByRoot.ts deleted file mode 100644 index 84993bf9a687..000000000000 --- a/packages/reqresp/src/handlers/beaconBlocksByRoot.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {allForks, phase0, Slot} from "@lodestar/types"; -import {IBeaconChain, IBeaconDb} from "../sharedTypes.js"; -import {ContextBytesType, EncodedPayload, EncodedPayloadType} from "../types.js"; -import {getSlotFromBytes} from "../utils/index.js"; - -export async function* onBeaconBlocksByRoot( - requestBody: phase0.BeaconBlocksByRootRequest, - chain: IBeaconChain, - db: IBeaconDb -): AsyncIterable> { - for (const blockRoot of requestBody) { - const root = blockRoot; - const summary = chain.forkChoice.getBlock(root); - let blockBytes: Uint8Array | null = null; - - // finalized block has summary in forkchoice but it stays in blockArchive db - if (summary) { - blockBytes = await db.block.getBinary(root); - } - - let slot: Slot | undefined = undefined; - if (!blockBytes) { - const blockEntry = await db.blockArchive.getBinaryEntryByRoot(root); - if (blockEntry) { - slot = blockEntry.key; - blockBytes = blockEntry.value; - } - } - if (blockBytes) { - yield { - type: EncodedPayloadType.bytes, - bytes: blockBytes, - contextBytes: { - type: ContextBytesType.ForkDigest, - forkSlot: slot ?? getSlotFromBytes(blockBytes), - }, - }; - } - } -} diff --git a/packages/reqresp/src/handlers/index.ts b/packages/reqresp/src/handlers/index.ts deleted file mode 100644 index f9f9a9f3e894..000000000000 --- a/packages/reqresp/src/handlers/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {allForks, altair, phase0, Root} from "@lodestar/types"; -import {EncodedPayload, EncodedPayloadType} from "../types.js"; -import {IBeaconChain, IBeaconDb} from "../sharedTypes.js"; -import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js"; -import {onBeaconBlocksByRoot} from "./beaconBlocksByRoot.js"; -import {onLightClientBootstrap} from "./lightClientBootstrap.js"; -import {onLightClientUpdatesByRange} from "./lightClientUpdatesByRange.js"; -import {onLightClientFinalityUpdate} from "./lightClientFinalityUpdate.js"; -import {onLightClientOptimisticUpdate} from "./lightClientOptimisticUpdate.js"; - -export type ReqRespHandlers = { - onStatus(): AsyncIterable>; - onBeaconBlocksByRange( - req: phase0.BeaconBlocksByRangeRequest - ): AsyncIterable>; - onBeaconBlocksByRoot( - req: phase0.BeaconBlocksByRootRequest - ): AsyncIterable>; - onLightClientBootstrap(req: Root): AsyncIterable>; - onLightClientUpdatesByRange( - req: altair.LightClientUpdatesByRange - ): AsyncIterable>; - onLightClientFinalityUpdate(): AsyncIterable>; - onLightClientOptimisticUpdate(): AsyncIterable>; -}; - -/** - * The ReqRespHandler module handles app-level requests / responses from other peers, - * fetching state from the chain and database as needed. - */ -export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconChain}): ReqRespHandlers { - return { - async *onStatus() { - yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; - }, - async *onBeaconBlocksByRange(req) { - yield* onBeaconBlocksByRange(req, chain, db); - }, - async *onBeaconBlocksByRoot(req) { - yield* onBeaconBlocksByRoot(req, chain, db); - }, - async *onLightClientBootstrap(req) { - yield* onLightClientBootstrap(req, chain); - }, - async *onLightClientUpdatesByRange(req) { - yield* onLightClientUpdatesByRange(req, chain); - }, - async *onLightClientFinalityUpdate() { - yield* onLightClientFinalityUpdate(chain); - }, - async *onLightClientOptimisticUpdate() { - yield* onLightClientOptimisticUpdate(chain); - }, - }; -} diff --git a/packages/reqresp/src/handlers/lightClientBootstrap.ts b/packages/reqresp/src/handlers/lightClientBootstrap.ts deleted file mode 100644 index 264d421ac780..000000000000 --- a/packages/reqresp/src/handlers/lightClientBootstrap.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {altair, Root} from "@lodestar/types"; -import {ResponseError} from "../response/index.js"; -import {IBeaconChain, LightClientServerError, LightClientServerErrorCode} from "../sharedTypes.js"; -import {RespStatus} from "../interface.js"; -import {EncodedPayload, EncodedPayloadType} from "../types.js"; - -export async function* onLightClientBootstrap( - requestBody: Root, - chain: IBeaconChain -): AsyncIterable> { - try { - yield { - type: EncodedPayloadType.ssz, - data: await chain.lightClientServer.getBootstrap(requestBody), - }; - } catch (e) { - if ((e as LightClientServerError).type?.code === LightClientServerErrorCode.RESOURCE_UNAVAILABLE) { - throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, (e as Error).message); - } else { - throw new ResponseError(RespStatus.SERVER_ERROR, (e as Error).message); - } - } -} diff --git a/packages/reqresp/src/handlers/lightClientFinalityUpdate.ts b/packages/reqresp/src/handlers/lightClientFinalityUpdate.ts deleted file mode 100644 index 773beb53986f..000000000000 --- a/packages/reqresp/src/handlers/lightClientFinalityUpdate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {altair} from "@lodestar/types"; -import {ResponseError} from "../response/index.js"; -import {IBeaconChain} from "../sharedTypes.js"; -import {RespStatus} from "../interface.js"; -import {EncodedPayload, EncodedPayloadType} from "../types.js"; - -export async function* onLightClientFinalityUpdate( - chain: IBeaconChain -): AsyncIterable> { - const finalityUpdate = chain.lightClientServer.getFinalityUpdate(); - if (finalityUpdate === null) { - throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest finality update available"); - } else { - yield { - type: EncodedPayloadType.ssz, - data: finalityUpdate, - }; - } -} diff --git a/packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts b/packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts deleted file mode 100644 index a246918dea17..000000000000 --- a/packages/reqresp/src/handlers/lightClientOptimisticUpdate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {altair} from "@lodestar/types"; -import {ResponseError} from "../response/index.js"; -import {IBeaconChain} from "../sharedTypes.js"; -import {RespStatus} from "../interface.js"; -import {EncodedPayload, EncodedPayloadType} from "../types.js"; - -export async function* onLightClientOptimisticUpdate( - chain: IBeaconChain -): AsyncIterable> { - const optimisticUpdate = chain.lightClientServer.getOptimisticUpdate(); - if (optimisticUpdate === null) { - throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest optimistic update available"); - } else { - yield { - type: EncodedPayloadType.ssz, - data: optimisticUpdate, - }; - } -} diff --git a/packages/reqresp/src/handlers/lightClientUpdatesByRange.ts b/packages/reqresp/src/handlers/lightClientUpdatesByRange.ts deleted file mode 100644 index 7699cadc435b..000000000000 --- a/packages/reqresp/src/handlers/lightClientUpdatesByRange.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {altair} from "@lodestar/types"; -import {MAX_REQUEST_LIGHT_CLIENT_UPDATES} from "@lodestar/params"; -import {ResponseError} from "../response/errors.js"; -import {EncodedPayload, EncodedPayloadType} from "../types.js"; -import {IBeaconChain, LightClientServerError, LightClientServerErrorCode} from "../sharedTypes.js"; -import {RespStatus} from "../interface.js"; - -export async function* onLightClientUpdatesByRange( - requestBody: altair.LightClientUpdatesByRange, - chain: IBeaconChain -): AsyncIterable> { - const count = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, requestBody.count); - for (let period = requestBody.startPeriod; period < requestBody.startPeriod + count; period++) { - try { - yield { - type: EncodedPayloadType.ssz, - data: await chain.lightClientServer.getUpdate(period), - }; - } catch (e) { - if ((e as LightClientServerError).type?.code === LightClientServerErrorCode.RESOURCE_UNAVAILABLE) { - throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, (e as Error).message); - } else { - throw new ResponseError(RespStatus.SERVER_ERROR, (e as Error).message); - } - } - } -} diff --git a/packages/reqresp/src/index.ts b/packages/reqresp/src/index.ts index da92c285cc90..f9ecccadf746 100644 --- a/packages/reqresp/src/index.ts +++ b/packages/reqresp/src/index.ts @@ -1,6 +1,7 @@ -export {ReqResp, IReqRespOptions} from "./reqResp.js"; -export {ReqRespHandlers, getReqRespHandlers} from "./handlers/index.js"; +export {ReqResp} from "./ReqResp.js"; +export {getMetrics, Metrics, MetricsRegister} from "./metrics.js"; +export {Encoding as ReqRespEncoding, Method as ReqRespMethod} from "./types.js"; // Expose enums renamed +export * from "./types.js"; export * from "./interface.js"; export * from "./constants.js"; -export {RequestTypedContainer} from "./types.js"; // To type-safe reqResp event listeners -export {Encoding as ReqRespEncoding, Method as ReqRespMethod} from "./types.js"; // Expose enums renamed +export * from "./response/errors.js"; diff --git a/packages/reqresp/src/interface.ts b/packages/reqresp/src/interface.ts index e69436cddcd1..e9103f0a8f14 100644 --- a/packages/reqresp/src/interface.ts +++ b/packages/reqresp/src/interface.ts @@ -4,9 +4,11 @@ import {ForkName} from "@lodestar/params"; import {IBeaconConfig} from "@lodestar/config"; import {allForks, altair, phase0} from "@lodestar/types"; import {ILogger} from "@lodestar/utils"; -import {ReqRespHandlers} from "./handlers/index.js"; import {INetworkEventBus, IPeerRpcScoreStore, MetadataController, PeersData} from "./sharedTypes.js"; import {Metrics} from "./metrics.js"; +import {Method, ProtocolDefinition, RequestTypedContainer} from "./types.js"; +import {RequestError} from "./request/errors.js"; +import {ReqRespProtocolModules} from "./ReqRespProtocol.js"; export interface IReqResp { start(): void; @@ -25,24 +27,36 @@ export interface IReqResp { lightClientOptimisticUpdate(peerId: PeerId): Promise; lightClientFinalityUpdate(peerId: PeerId): Promise; lightClientUpdate(peerId: PeerId, request: altair.LightClientUpdatesByRange): Promise; + registerProtocol(protocol: ProtocolDefinition): void; } -export interface IReqRespModules { +export interface ReqRespEventsHandlers { + onIncomingRequestBody(modules: ReqRespProtocolModules, req: RequestTypedContainer, peerId: PeerId): void; + onOutgoingReqRespError(modules: ReqRespProtocolModules, peerId: PeerId, method: Method, error: RequestError): void; + onIncomingRequest(modules: ReqRespProtocolModules, peerId: PeerId, method: Method): void; +} + +export interface ReqRespModules { config: IBeaconConfig; libp2p: Libp2p; peersData: PeersData; logger: ILogger; metadata: MetadataController; - reqRespHandlers: ReqRespHandlers; peerRpcScores: IPeerRpcScoreStore; networkEventBus: INetworkEventBus; metrics: Metrics | null; + inboundRateLimiter: RateLimiter; +} + +export interface ReqRespHandlerContext { + modules: ReqRespProtocolModules; + eventsHandlers: ReqRespEventsHandlers; } /** * Rate limiter interface for inbound and outbound requests. */ -export interface IRateLimiter { +export interface RateLimiter { /** Allow to request or response based on rate limit params configured. */ allowRequest(peerId: PeerId): boolean; /** Rate limit check for block count */ diff --git a/packages/reqresp/src/messages/index.ts b/packages/reqresp/src/messages/index.ts new file mode 100644 index 000000000000..1e17721f08ae --- /dev/null +++ b/packages/reqresp/src/messages/index.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {BeaconBlocksByRoot} from "./v1/BeaconBlocksByRoot.js"; +import {BeaconBlocksByRange} from "./v1/BeaconBlocksByRange.js"; +import {Goodbye} from "./v1/Goodbye.js"; +import {LightClientBootstrap} from "./v1/LightClientBootstrap.js"; +import {LightClientFinalityUpdate} from "./v1/LightClientFinalityUpdate.js"; +import {LightClientOptimisticUpdate} from "./v1/LightClientOptimisticUpdate.js"; +import {LightClientUpdatesByRange} from "./v1/LightClientUpdatesByRange.js"; +import {Metadata} from "./v1/Metadata.js"; +import {Ping} from "./v1/Ping.js"; +import {Status} from "./v1/Status.js"; +import {BeaconBlocksByRangeV2} from "./v2/BeaconBlocksByRange.js"; +import {BeaconBlocksByRootV2} from "./v2/BeaconBlocksByRoot.js"; +import {MetadataV2} from "./v2/Metadata.js"; + +export default { + v1: { + BeaconBlocksByRoot, + BeaconBlocksByRange, + Goodbye, + LightClientBootstrap, + LightClientFinalityUpdate, + LightClientOptimisticUpdate, + LightClientUpdatesByRange, + Metadata, + Ping, + Status, + }, + v2: { + BeaconBlocksByRange: BeaconBlocksByRangeV2, + BeaconBlocksByRoot: BeaconBlocksByRootV2, + Metadata: MetadataV2, + }, +}; diff --git a/packages/reqresp/src/messages/utils.ts b/packages/reqresp/src/messages/utils.ts new file mode 100644 index 000000000000..a889c2925ce5 --- /dev/null +++ b/packages/reqresp/src/messages/utils.ts @@ -0,0 +1,14 @@ +import {ForkName} from "@lodestar/params"; +import {ReqRespModules} from "../interface.js"; +import {ContextBytesType, ContextBytesFactory} from "../types.js"; + +export function getContextBytesLightclient( + forkFromResponse: (response: T) => ForkName, + modules: ReqRespModules +): ContextBytesFactory { + return { + type: ContextBytesType.ForkDigest, + forkDigestContext: modules.config, + forkFromResponse, + }; +} diff --git a/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts new file mode 100644 index 000000000000..6ed9960ecde3 --- /dev/null +++ b/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts @@ -0,0 +1,28 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {RespStatus} from "../../interface.js"; +import {ResponseError} from "../../response/errors.js"; +import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRange: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRangeRequest, + allForks.SignedBeaconBlock +> = (handler) => { + return { + method: Method.BeaconBlocksByRange, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* beaconBlocksByRangeHandler(context, req, peerId) { + if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + + yield* handler(req, peerId); + }, + requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: false, + }; +}; diff --git a/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts new file mode 100644 index 000000000000..06669790eeb1 --- /dev/null +++ b/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts @@ -0,0 +1,29 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {toHex} from "@lodestar/utils"; +import {RespStatus} from "../../interface.js"; +import {ResponseError} from "../../response/errors.js"; +import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRoot: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRootRequest, + allForks.SignedBeaconBlock +> = (handler) => { + return { + method: Method.BeaconBlocksByRange, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* beaconBlocksByRootHandler(context, req, peerId) { + if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + + yield* handler(req, peerId); + }, + requestType: () => ssz.phase0.BeaconBlocksByRootRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: false, + }; +}; diff --git a/packages/reqresp/src/messages/v1/Goodbye.ts b/packages/reqresp/src/messages/v1/Goodbye.ts new file mode 100644 index 000000000000..f31306fdcc03 --- /dev/null +++ b/packages/reqresp/src/messages/v1/Goodbye.ts @@ -0,0 +1,28 @@ +import {phase0, ssz} from "@lodestar/types"; +import { + ContextBytesType, + EncodedPayloadType, + Encoding, + Method, + ProtocolDefinitionGenerator, + Version, +} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Goodbye: ProtocolDefinitionGenerator = (_handler) => { + return { + method: Method.Status, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* goodbyeHandler(context, req, peerId) { + context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Goodbye, body: req}, peerId); + + yield {type: EncodedPayloadType.ssz, data: context.modules.metadata.seqNumber}; + }, + requestType: () => ssz.phase0.Goodbye, + responseType: () => ssz.phase0.Goodbye, + renderRequestBody: (req) => req.toString(10), + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/LightClientBootstrap.ts b/packages/reqresp/src/messages/v1/LightClientBootstrap.ts new file mode 100644 index 000000000000..f0026ce38522 --- /dev/null +++ b/packages/reqresp/src/messages/v1/LightClientBootstrap.ts @@ -0,0 +1,24 @@ +import {altair, Root, ssz} from "@lodestar/types"; +import {toHex} from "@lodestar/utils"; +import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getContextBytesLightclient} from "../utils.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const LightClientBootstrap: ProtocolDefinitionGenerator = ( + handler, + modules +) => { + return { + method: Method.LightClientBootstrap, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* lightClientBootstrapHandler(context, req, peerId) { + yield* handler(req, peerId); + }, + requestType: () => ssz.Root, + responseType: () => ssz.altair.LightClientBootstrap, + renderRequestBody: (req) => toHex(req), + contextBytes: getContextBytesLightclient((bootstrap) => modules.config.getForkName(bootstrap.header.slot), modules), + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts b/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts new file mode 100644 index 000000000000..c92f71036001 --- /dev/null +++ b/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts @@ -0,0 +1,22 @@ +import {altair, ssz} from "@lodestar/types"; +import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getContextBytesLightclient} from "../utils.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const LightClientFinalityUpdate: ProtocolDefinitionGenerator = ( + handler, + modules +) => { + return { + method: Method.LightClientFinalityUpdate, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* statusHandler(_context, req, peerId) { + yield* handler(req, peerId); + }, + requestType: () => null, + responseType: () => ssz.altair.LightClientFinalityUpdate, + contextBytes: getContextBytesLightclient((update) => modules.config.getForkName(update.signatureSlot), modules), + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts b/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts new file mode 100644 index 000000000000..b29208978e14 --- /dev/null +++ b/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts @@ -0,0 +1,22 @@ +import {altair, ssz} from "@lodestar/types"; +import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getContextBytesLightclient} from "../utils.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const LightClientOptimisticUpdate: ProtocolDefinitionGenerator = ( + handler, + modules +) => { + return { + method: Method.LightClientOptimisticUpdate, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* statusHandler(_context, req, peerId) { + yield* handler(req, peerId); + }, + requestType: () => null, + responseType: () => ssz.altair.LightClientOptimisticUpdate, + contextBytes: getContextBytesLightclient((update) => modules.config.getForkName(update.signatureSlot), modules), + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts b/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts new file mode 100644 index 000000000000..0d1bd593a625 --- /dev/null +++ b/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts @@ -0,0 +1,23 @@ +import {altair, ssz} from "@lodestar/types"; +import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getContextBytesLightclient} from "../utils.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const LightClientUpdatesByRange: ProtocolDefinitionGenerator< + altair.LightClientUpdatesByRange, + altair.LightClientUpdate +> = (handler, modules) => { + return { + method: Method.LightClientUpdatesByRange, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* statusHandler(_context, req, peerId) { + yield* handler(req, peerId); + }, + requestType: () => ssz.altair.LightClientUpdatesByRange, + responseType: () => ssz.altair.LightClientUpdate, + renderRequestBody: (req) => `${req.startPeriod},${req.count}`, + contextBytes: getContextBytesLightclient((update) => modules.config.getForkName(update.signatureSlot), modules), + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/Metadata.ts b/packages/reqresp/src/messages/v1/Metadata.ts new file mode 100644 index 000000000000..0337a7d42c26 --- /dev/null +++ b/packages/reqresp/src/messages/v1/Metadata.ts @@ -0,0 +1,27 @@ +import {allForks, ssz} from "@lodestar/types"; +import { + ContextBytesType, + EncodedPayloadType, + Encoding, + Method, + ProtocolDefinitionGenerator, + Version, +} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Metadata: ProtocolDefinitionGenerator = (_handler, modules) => { + return { + method: Method.Metadata, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* metadataHandler(context, req, peerId) { + context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Metadata, body: req}, peerId); + + yield {type: EncodedPayloadType.ssz, data: modules.metadata.json}; + }, + requestType: () => null, + responseType: () => ssz.phase0.Metadata, + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/Ping.ts b/packages/reqresp/src/messages/v1/Ping.ts new file mode 100644 index 000000000000..237ef7afd90f --- /dev/null +++ b/packages/reqresp/src/messages/v1/Ping.ts @@ -0,0 +1,28 @@ +import {phase0, ssz} from "@lodestar/types"; +import { + ContextBytesType, + EncodedPayloadType, + Encoding, + Method, + ProtocolDefinitionGenerator, + Version, +} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Ping: ProtocolDefinitionGenerator = (_handler, modules) => { + return { + method: Method.Status, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* pingHandler(context, req, peerId) { + context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Ping, body: req}, peerId); + + yield {type: EncodedPayloadType.ssz, data: modules.metadata.seqNumber}; + }, + requestType: () => ssz.phase0.Ping, + responseType: () => ssz.phase0.Ping, + renderRequestBody: (req) => req.toString(10), + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v1/Status.ts b/packages/reqresp/src/messages/v1/Status.ts new file mode 100644 index 000000000000..055cf2dc9c14 --- /dev/null +++ b/packages/reqresp/src/messages/v1/Status.ts @@ -0,0 +1,20 @@ +import {phase0, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Status: ProtocolDefinitionGenerator = (handler) => { + return { + method: Method.Status, + version: Version.V1, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* statusHandler(context, req, peerId) { + context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Status, body: req}, peerId); + + yield* handler(req, peerId); + }, + requestType: () => ssz.phase0.Status, + responseType: () => ssz.phase0.Status, + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts new file mode 100644 index 000000000000..16012c2ef543 --- /dev/null +++ b/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts @@ -0,0 +1,32 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {RespStatus} from "../../interface.js"; +import {ResponseError} from "../../response/errors.js"; +import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRangeV2: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRangeRequest, + allForks.SignedBeaconBlock +> = (handler, modules) => { + return { + method: Method.BeaconBlocksByRange, + version: Version.V2, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* beaconBlocksByRangeV2Handler(context, req, peerId) { + if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + + yield* handler(req, peerId); + }, + requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkDigestContext: modules.config, + forkFromResponse: (block) => modules.config.getForkName(block.message.slot), + }, + isSingleResponse: false, + }; +}; diff --git a/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts new file mode 100644 index 000000000000..1afde6e5b8f5 --- /dev/null +++ b/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts @@ -0,0 +1,33 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {toHex} from "@lodestar/utils"; +import {RespStatus} from "../../interface.js"; +import {ResponseError} from "../../response/errors.js"; +import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRootV2: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRootRequest, + allForks.SignedBeaconBlock +> = (handler, modules) => { + return { + method: Method.BeaconBlocksByRoot, + version: Version.V2, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* beaconBlocksByRootV2Handler(context, req, peerId) { + if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + + yield* handler(req, peerId); + }, + requestType: () => ssz.phase0.BeaconBlocksByRootRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), + contextBytes: { + type: ContextBytesType.ForkDigest, + forkDigestContext: modules.config, + forkFromResponse: (block) => modules.config.getForkName(block.message.slot), + }, + isSingleResponse: false, + }; +}; diff --git a/packages/reqresp/src/messages/v2/Metadata.ts b/packages/reqresp/src/messages/v2/Metadata.ts new file mode 100644 index 000000000000..2f755ad80bfc --- /dev/null +++ b/packages/reqresp/src/messages/v2/Metadata.ts @@ -0,0 +1,27 @@ +import {allForks, ssz} from "@lodestar/types"; +import { + ContextBytesType, + EncodedPayloadType, + Encoding, + Method, + ProtocolDefinitionGenerator, + Version, +} from "../../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const MetadataV2: ProtocolDefinitionGenerator = (_handler, modules) => { + return { + method: Method.Metadata, + version: Version.V2, + encoding: Encoding.SSZ_SNAPPY, + handler: async function* metadataV2Handler(context, req, peerId) { + context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Metadata, body: req}, peerId); + + yield {type: EncodedPayloadType.ssz, data: modules.metadata.json}; + }, + requestType: () => null, + responseType: () => ssz.altair.Metadata, + contextBytes: {type: ContextBytesType.Empty}, + isSingleResponse: true, + }; +}; diff --git a/packages/reqresp/src/response/rateLimiter.ts b/packages/reqresp/src/rate_limiter/RateLimiter.ts similarity index 80% rename from packages/reqresp/src/response/rateLimiter.ts rename to packages/reqresp/src/rate_limiter/RateLimiter.ts index 7faf03461658..0f45e6cac205 100644 --- a/packages/reqresp/src/response/rateLimiter.ts +++ b/packages/reqresp/src/rate_limiter/RateLimiter.ts @@ -1,11 +1,11 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ILogger, MapDef} from "@lodestar/utils"; -import {IRateLimiter} from "../interface.js"; +import {RateLimiter} from "../interface.js"; import {Metrics} from "../metrics.js"; -import {RateTracker} from "../rateTracker.js"; import {IPeerRpcScoreStore, PeerAction} from "../sharedTypes.js"; +import {RateTracker} from "./RateTracker.js"; -interface IRateLimiterModules { +interface RateLimiterModules { logger: ILogger; peerRpcScores: IPeerRpcScoreStore; metrics: Metrics | null; @@ -18,7 +18,7 @@ interface IRateLimiterModules { * - blockCountTotalLimit: maximum block count we can serve for all peers within rateTrackerTimeoutMs * - rateTrackerTimeoutMs: the time period we want to track total requests or objects, normally 1 min */ -export type RateLimiterOpts = { +export type RateLimiterOptions = { requestCountPeerLimit: number; blockCountPeerLimit: number; blockCountTotalLimit: number; @@ -31,25 +31,11 @@ const CHECK_DISCONNECTED_PEERS_INTERVAL_MS = 10 * 60 * 1000; /** Peers don't request us for 5 mins are considered disconnected */ const DISCONNECTED_TIMEOUT_MS = 5 * 60 * 1000; -/** - * Default value for RateLimiterOpts - * - requestCountPeerLimit: allow to serve 50 requests per peer within 1 minute - * - blockCountPeerLimit: allow to serve 500 blocks per peer within 1 minute - * - blockCountTotalLimit: allow to serve 2000 (blocks) for all peer within 1 minute (4 x blockCountPeerLimit) - * - rateTrackerTimeoutMs: 1 minute - */ -export const defaultRateLimiterOpts = { - requestCountPeerLimit: 50, - blockCountPeerLimit: 500, - blockCountTotalLimit: 2000, - rateTrackerTimeoutMs: 60 * 1000, -}; - /** * This class is singleton, it has per-peer request count rate tracker and block count rate tracker * and a block count rate tracker for all peers (this is lodestar specific). */ -export class InboundRateLimiter implements IRateLimiter { +export class InboundRateLimiter implements RateLimiter { private readonly logger: ILogger; private readonly peerRpcScores: IPeerRpcScoreStore; private readonly metrics: Metrics | null; @@ -64,17 +50,34 @@ export class InboundRateLimiter implements IRateLimiter { private lastSeenRequestsByPeer: Map; /** Interval to check lastSeenMessagesByPeer */ private cleanupInterval: NodeJS.Timeout | undefined = undefined; + private options: RateLimiterOptions; + + /** + * Default value for RateLimiterOpts + * - requestCountPeerLimit: allow to serve 50 requests per peer within 1 minute + * - blockCountPeerLimit: allow to serve 500 blocks per peer within 1 minute + * - blockCountTotalLimit: allow to serve 2000 (blocks) for all peer within 1 minute (4 x blockCountPeerLimit) + * - rateTrackerTimeoutMs: 1 minute + */ + static defaults: RateLimiterOptions = { + requestCountPeerLimit: 50, + blockCountPeerLimit: 500, + blockCountTotalLimit: 2000, + rateTrackerTimeoutMs: 60 * 1000, + }; + + constructor(options: Partial, modules: RateLimiterModules) { + this.options = {...InboundRateLimiter.defaults, ...options}; - constructor(opts: RateLimiterOpts, modules: IRateLimiterModules) { this.requestCountTrackersByPeer = new MapDef( - () => new RateTracker({limit: opts.requestCountPeerLimit, timeoutMs: opts.rateTrackerTimeoutMs}) + () => new RateTracker({limit: this.options.requestCountPeerLimit, timeoutMs: this.options.rateTrackerTimeoutMs}) ); this.blockCountTotalTracker = new RateTracker({ - limit: opts.blockCountTotalLimit, - timeoutMs: opts.rateTrackerTimeoutMs, + limit: this.options.blockCountTotalLimit, + timeoutMs: this.options.rateTrackerTimeoutMs, }); this.blockCountTrackersByPeer = new MapDef( - () => new RateTracker({limit: opts.blockCountPeerLimit, timeoutMs: opts.rateTrackerTimeoutMs}) + () => new RateTracker({limit: this.options.blockCountPeerLimit, timeoutMs: this.options.rateTrackerTimeoutMs}) ); this.logger = modules.logger; this.peerRpcScores = modules.peerRpcScores; diff --git a/packages/reqresp/src/rateTracker.ts b/packages/reqresp/src/rate_limiter/RateTracker.ts similarity index 100% rename from packages/reqresp/src/rateTracker.ts rename to packages/reqresp/src/rate_limiter/RateTracker.ts diff --git a/packages/reqresp/src/rate_limiter/index.ts b/packages/reqresp/src/rate_limiter/index.ts new file mode 100644 index 000000000000..f85a8147f3aa --- /dev/null +++ b/packages/reqresp/src/rate_limiter/index.ts @@ -0,0 +1,2 @@ +export * from "./RateLimiter.js"; +export * from "./RateTracker.js"; diff --git a/packages/reqresp/src/reqResp.ts b/packages/reqresp/src/reqResp.ts deleted file mode 100644 index 79bd8d593070..000000000000 --- a/packages/reqresp/src/reqResp.ts +++ /dev/null @@ -1,381 +0,0 @@ -import {PeerId} from "@libp2p/interface-peer-id"; -import {Type} from "@chainsafe/ssz"; -import {ForkName} from "@lodestar/params"; -import {allForks, altair, phase0, Root, Slot, ssz} from "@lodestar/types"; -import {toHex} from "@lodestar/utils"; -import {RespStatus} from "./interface.js"; -import {IReqResp, IReqRespModules, IRateLimiter} from "./interface.js"; -import {ResponseError} from "./response/index.js"; -import {assertSequentialBlocksInRange} from "./utils/index.js"; -import {ReqRespHandlers} from "./handlers/index.js"; -import { - Method, - Version, - Encoding, - ContextBytesType, - ContextBytesFactory, - EncodedPayload, - EncodedPayloadType, - RequestTypedContainer, -} from "./types.js"; -import {InboundRateLimiter, RateLimiterOpts} from "./response/rateLimiter.js"; -import {ReqRespProtocol} from "./reqRespProtocol.js"; -import {RequestError} from "./request/errors.js"; -import {onOutgoingReqRespError} from "./score.js"; -import {timeoutOptions} from "./constants.js"; -import {MetadataController, IPeerRpcScoreStore, INetworkEventBus, NetworkEvent} from "./sharedTypes.js"; - -export type IReqRespOptions = Partial; - -/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ -export type ReqRespBlockResponse = { - /** Deserialized data of allForks.SignedBeaconBlock */ - bytes: Uint8Array; - slot: Slot; -}; - -/** - * Implementation of Ethereum Consensus p2p Req/Resp domain. - * For the spec that this code is based on, see: - * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain - * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain - */ -export class ReqResp extends ReqRespProtocol implements IReqResp { - private reqRespHandlers: ReqRespHandlers; - private metadataController: MetadataController; - private peerRpcScores: IPeerRpcScoreStore; - private inboundRateLimiter: IRateLimiter; - private networkEventBus: INetworkEventBus; - - constructor(modules: IReqRespModules, options: IReqRespOptions & RateLimiterOpts) { - super(modules, options); - - const {reqRespHandlers, config} = modules; - - // Single chunk protocols - - this.registerProtocol({ - method: Method.Status, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: this.onStatus.bind(this), - requestType: () => ssz.phase0.Status, - responseType: () => ssz.phase0.Status, - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }); - - this.registerProtocol({ - method: Method.Goodbye, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: this.onGoodbye.bind(this), - requestType: () => ssz.phase0.Goodbye, - responseType: () => ssz.phase0.Goodbye, - renderRequestBody: (req) => req.toString(10), - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }); - - this.registerProtocol({ - method: Method.Ping, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: this.onPing.bind(this), - requestType: () => ssz.phase0.Ping, - responseType: () => ssz.phase0.Ping, - renderRequestBody: (req) => req.toString(10), - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }); - - // V1 -> phase0.Metadata, V2 -> altair.Metadata - for (const [version, responseType] of [ - [Version.V1, ssz.phase0.Metadata], - [Version.V2, ssz.altair.Metadata], - ] as [Version, Type][]) { - this.registerProtocol({ - method: Method.Metadata, - version, - encoding: Encoding.SSZ_SNAPPY, - handler: this.onMetadata.bind(this), - requestType: () => null, - responseType: () => responseType, - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }); - } - - // Block by protocols - - const contextBytesEmpty = {type: ContextBytesType.Empty}; - - const contextBytesBlocksByV2: ContextBytesFactory = { - type: ContextBytesType.ForkDigest, - forkDigestContext: config, - forkFromResponse: (block) => config.getForkName(block.message.slot), - }; - - for (const [version, contextBytes] of [ - [Version.V1, contextBytesEmpty], - [Version.V2, contextBytesBlocksByV2], - ] as [Version, ContextBytesFactory][]) { - this.registerProtocol({ - method: Method.BeaconBlocksByRange, - version, - encoding: Encoding.SSZ_SNAPPY, - handler: this.onBeaconBlocksByRange.bind(this), - requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, - responseType: (forkName) => ssz[forkName].SignedBeaconBlock, - renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, - contextBytes, - isSingleResponse: false, - }); - - this.registerProtocol({ - method: Method.BeaconBlocksByRoot, - version, - encoding: Encoding.SSZ_SNAPPY, - handler: this.onBeaconBlocksByRoot.bind(this), - requestType: () => ssz.phase0.BeaconBlocksByRootRequest, - responseType: (forkName) => ssz[forkName].SignedBeaconBlock, - renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), - contextBytes, - isSingleResponse: false, - }); - } - - // Lightclient methods - - function getContextBytesLightclient(forkFromResponse: (response: T) => ForkName): ContextBytesFactory { - return { - type: ContextBytesType.ForkDigest, - forkDigestContext: config, - forkFromResponse, - }; - } - - this.registerProtocol({ - method: Method.LightClientBootstrap, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: reqRespHandlers.onLightClientBootstrap, - requestType: () => ssz.Root, - responseType: () => ssz.altair.LightClientBootstrap, - renderRequestBody: (req) => toHex(req), - contextBytes: getContextBytesLightclient((bootstrap) => config.getForkName(bootstrap.header.slot)), - isSingleResponse: true, - }); - - this.registerProtocol({ - method: Method.LightClientUpdatesByRange, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: reqRespHandlers.onLightClientUpdatesByRange, - requestType: () => ssz.altair.LightClientUpdatesByRange, - responseType: () => ssz.altair.LightClientUpdate, - renderRequestBody: (req) => `${req.startPeriod},${req.count}`, - contextBytes: getContextBytesLightclient((update) => config.getForkName(update.signatureSlot)), - isSingleResponse: false, - }); - - this.registerProtocol({ - method: Method.LightClientFinalityUpdate, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: reqRespHandlers.onLightClientFinalityUpdate, - requestType: () => null, - responseType: () => ssz.altair.LightClientFinalityUpdate, - contextBytes: getContextBytesLightclient((update) => config.getForkName(update.signatureSlot)), - isSingleResponse: true, - }); - - this.registerProtocol({ - method: Method.LightClientOptimisticUpdate, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: reqRespHandlers.onLightClientOptimisticUpdate, - requestType: () => null, - responseType: () => ssz.altair.LightClientOptimisticUpdate, - contextBytes: getContextBytesLightclient((update) => config.getForkName(update.signatureSlot)), - isSingleResponse: true, - }); - - this.reqRespHandlers = modules.reqRespHandlers; - this.metadataController = modules.metadata; - this.peerRpcScores = modules.peerRpcScores; - this.inboundRateLimiter = new InboundRateLimiter(options, {...modules}); - this.networkEventBus = modules.networkEventBus; - } - - async start(): Promise { - await super.start(); - this.inboundRateLimiter.start(); - } - - async stop(): Promise { - await super.stop(); - this.inboundRateLimiter.stop(); - } - - pruneOnPeerDisconnect(peerId: PeerId): void { - this.inboundRateLimiter.prune(peerId); - } - - async status(peerId: PeerId, request: phase0.Status): Promise { - return await this.sendRequest(peerId, Method.Status, [Version.V1], request); - } - - async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { - await this.sendRequest(peerId, Method.Goodbye, [Version.V1], request); - } - - async ping(peerId: PeerId): Promise { - return await this.sendRequest( - peerId, - Method.Ping, - [Version.V1], - this.metadataController.seqNumber - ); - } - - async metadata(peerId: PeerId, fork?: ForkName): Promise { - // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version - const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; - return await this.sendRequest(peerId, Method.Metadata, versions, null); - } - - async beaconBlocksByRange( - peerId: PeerId, - request: phase0.BeaconBlocksByRangeRequest - ): Promise { - const blocks = await this.sendRequest( - peerId, - Method.BeaconBlocksByRange, - [Version.V2, Version.V1], // Prioritize V2 - request, - request.count - ); - assertSequentialBlocksInRange(blocks, request); - return blocks; - } - - async beaconBlocksByRoot( - peerId: PeerId, - request: phase0.BeaconBlocksByRootRequest - ): Promise { - return await this.sendRequest( - peerId, - Method.BeaconBlocksByRoot, - [Version.V2, Version.V1], // Prioritize V2 - request, - request.length - ); - } - - async lightClientBootstrap(peerId: PeerId, request: Root): Promise { - return await this.sendRequest( - peerId, - Method.LightClientBootstrap, - [Version.V1], - request - ); - } - - async lightClientOptimisticUpdate(peerId: PeerId): Promise { - return await this.sendRequest( - peerId, - Method.LightClientOptimisticUpdate, - [Version.V1], - null - ); - } - - async lightClientFinalityUpdate(peerId: PeerId): Promise { - return await this.sendRequest( - peerId, - Method.LightClientFinalityUpdate, - [Version.V1], - null - ); - } - - async lightClientUpdate( - peerId: PeerId, - request: altair.LightClientUpdatesByRange - ): Promise { - return await this.sendRequest( - peerId, - Method.LightClientUpdatesByRange, - [Version.V1], - request, - request.count - ); - } - - /** - * @override Rate limit requests before decoding request body - */ - protected onIncomingRequest(peerId: PeerId, method: Method): void { - if (method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - } - - protected onOutgoingReqRespError(peerId: PeerId, method: Method, error: RequestError): void { - const peerAction = onOutgoingReqRespError(error, method); - if (peerAction !== null) { - this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); - } - } - - private onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { - // Allow onRequest to return and close the stream - // For Goodbye there may be a race condition where the listener of `receivedGoodbye` - // disconnects in the same syncronous call, preventing the stream from ending cleanly - setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); - } - - private async *onStatus(req: phase0.Status, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Status, body: req}, peerId); - yield* this.reqRespHandlers.onStatus(); - } - - private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); - yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; - } - - private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); - yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; - } - - private async *onMetadata(req: null, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); - - // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property - // It's safe to return altair.Metadata here for all versions - yield {type: EncodedPayloadType.ssz, data: this.metadataController.json}; - } - - private async *onBeaconBlocksByRange( - req: phase0.BeaconBlocksByRangeRequest, - peerId: PeerId - ): AsyncIterable> { - if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - yield* this.reqRespHandlers.onBeaconBlocksByRange(req); - } - - private async *onBeaconBlocksByRoot( - req: phase0.BeaconBlocksByRootRequest, - peerId: PeerId - ): AsyncIterable> { - if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - yield* this.reqRespHandlers.onBeaconBlocksByRoot(req); - } -} diff --git a/packages/reqresp/src/response/index.ts b/packages/reqresp/src/response/index.ts index da6bbf1e4ef6..302a2f24137b 100644 --- a/packages/reqresp/src/response/index.ts +++ b/packages/reqresp/src/response/index.ts @@ -8,12 +8,13 @@ import {prettyPrintPeerId} from "../utils/index.js"; import {ProtocolDefinition} from "../types.js"; import {requestDecode} from "../encoders/requestDecode.js"; import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js"; -import {RespStatus} from "../interface.js"; +import {ReqRespHandlerContext, RespStatus} from "../interface.js"; import {ResponseError} from "./errors.js"; export {ResponseError}; export interface HandleRequestOpts { + context: ReqRespHandlerContext; logger: ILogger; stream: Stream; peerId: PeerId; @@ -35,6 +36,7 @@ export interface HandleRequestOpts { * 4b. On error, encode and write an error `` and stop */ export async function handleRequest({ + context, logger, stream, peerId, @@ -67,7 +69,7 @@ export async function handleRequest({ logger.debug("Resp received request", {...logCtx, body: protocol.renderRequestBody?.(requestBody)}); yield* pipe( - protocol.handler(requestBody, peerId), + protocol.handler(context, requestBody, peerId), // NOTE: Do not log the resp chunk contents, logs get extremely cluttered // Note: Not logging on each chunk since after 1 year it hasn't add any value when debugging // onChunk(() => logger.debug("Resp sending chunk", logCtx)), diff --git a/packages/reqresp/src/sharedTypes.ts b/packages/reqresp/src/sharedTypes.ts index b0e1119c4aa2..50be4ec9ac79 100644 --- a/packages/reqresp/src/sharedTypes.ts +++ b/packages/reqresp/src/sharedTypes.ts @@ -4,8 +4,7 @@ import StrictEventEmitter from "strict-event-emitter-types"; import {ENR} from "@chainsafe/discv5"; import {BitArray} from "@chainsafe/ssz"; import {ForkName} from "@lodestar/params"; -import {allForks, altair, Epoch, phase0, Root, RootHex, Slot} from "@lodestar/types"; -import {LodestarError} from "@lodestar/utils"; +import {allForks, altair, Epoch, phase0} from "@lodestar/types"; import {Encoding, RequestTypedContainer} from "./types.js"; // These interfaces are shared among beacon-node package. @@ -111,39 +110,3 @@ export interface MetadataController { start(enr: ENR | undefined, currentFork: ForkName): void; updateEth2Field(epoch: Epoch): void; } - -export interface IBeaconChain { - getStatus(): phase0.Status; - - readonly lightClientServer: { - getUpdate(period: number): Promise; - getOptimisticUpdate(): altair.LightClientOptimisticUpdate | null; - getFinalityUpdate(): altair.LightClientFinalityUpdate | null; - getBootstrap(blockRoot: Uint8Array): Promise; - }; - readonly forkChoice: { - // For our use case we don't need the full block in this package - getBlock(blockRoot: Root): Uint8Array | null; - getHeadRoot(): RootHex; - iterateAncestorBlocks(blockRoot: RootHex): IterableIterator<{slot: Slot; blockRoot: RootHex}>; - }; -} - -export enum LightClientServerErrorCode { - RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE", -} - -export type LightClientServerErrorType = {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}; - -export class LightClientServerError extends LodestarError {} - -export interface IBeaconDb { - readonly block: { - getBinary: (blockRoot: Uint8Array) => Promise; - }; - readonly blockArchive: { - getBinaryEntryByRoot(blockRoot: Uint8Array): Promise<{key: Slot; value: Uint8Array | null}>; - decodeKey(data: Uint8Array): number; - binaryEntriesStream(opts?: {gte: number; lt: number}): AsyncIterable<{key: Uint8Array; value: Uint8Array}>; - }; -} diff --git a/packages/reqresp/src/types.ts b/packages/reqresp/src/types.ts index cff335fdb370..b3416d4f3c71 100644 --- a/packages/reqresp/src/types.ts +++ b/packages/reqresp/src/types.ts @@ -3,6 +3,9 @@ import {Type} from "@chainsafe/ssz"; import {IForkConfig, IForkDigestContext} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; import {phase0, Slot} from "@lodestar/types"; +import {LodestarError} from "@lodestar/utils"; +import {ReqRespEventsHandlers, ReqRespHandlerContext, ReqRespModules} from "./interface.js"; +import {timeoutOptions} from "./constants.js"; export enum EncodedPayloadType { ssz, @@ -20,10 +23,16 @@ export type EncodedPayload = contextBytes: ContextBytes; }; -export type Handler = (requestBody: Req, peerId: PeerId) => AsyncIterable>; +export type HandlerWithContext = ( + context: ReqRespHandlerContext, + req: Req, + peerId: PeerId +) => AsyncIterable>; + +export type Handler = (req: Req, peerId: PeerId) => AsyncIterable>; export interface ProtocolDefinition extends Protocol { - handler: Handler; + handler: HandlerWithContext; // eslint-disable-next-line @typescript-eslint/no-explicit-any requestType: (fork: ForkName) => Type | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -33,6 +42,11 @@ export interface ProtocolDefinition extends Proto isSingleResponse: boolean; } +export type ProtocolDefinitionGenerator = ( + handler: Handler, + modules: ReqRespModules +) => ProtocolDefinition; + export const protocolPrefix = "/eth2/beacon_chain/req"; /** ReqResp protocol names or methods. Each Method can have multiple versions and encodings */ @@ -107,3 +121,13 @@ export enum ContextBytesType { /** A fixed-width 4 byte , set to the ForkDigest matching the chunk: compute_fork_digest(fork_version, genesis_validators_root) */ ForkDigest, } + +export type ReqRespOptions = typeof timeoutOptions & ReqRespEventsHandlers; + +export enum LightClientServerErrorCode { + RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE", +} + +export type LightClientServerErrorType = {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}; + +export class LightClientServerError extends LodestarError {} From c174fe79b0653f2406a41bad605ac22ee432e95d Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 17 Nov 2022 00:59:26 +0100 Subject: [PATCH 05/23] Fix reqresp usage --- packages/beacon-node/package.json | 1 + packages/beacon-node/src/network/events.ts | 2 +- packages/beacon-node/src/network/interface.ts | 2 +- packages/beacon-node/src/network/network.ts | 19 +-- packages/beacon-node/src/network/options.ts | 6 +- .../src/network/peers/peerManager.ts | 2 +- .../src/network/peers/peersData.ts | 2 +- .../reqresp/handlers/beaconBlocksByRange.ts | 151 ++++++++++++++++++ .../reqresp/handlers/beaconBlocksByRoot.ts | 41 +++++ .../src/network/reqresp/handlers/index.ts | 47 ++++++ .../reqresp/handlers/lightClientBootstrap.ts | 28 ++++ .../handlers/lightClientFinalityUpdate.ts | 17 ++ .../handlers/lightClientOptimisticUpdate.ts | 17 ++ .../handlers/lightClientUpdatesByRange.ts | 32 ++++ .../src/network/reqresp/handlers/onStatus.ts | 8 + .../beacon-node/src/network/reqresp/index.ts | 53 ++++++ packages/beacon-node/src/node/nodejs.ts | 3 +- 17 files changed, 414 insertions(+), 17 deletions(-) create mode 100644 packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/index.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts create mode 100644 packages/beacon-node/src/network/reqresp/handlers/onStatus.ts create mode 100644 packages/beacon-node/src/network/reqresp/index.ts diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index 1f3d1a4c6f37..fb3eca510f08 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -127,6 +127,7 @@ "@lodestar/types": "^1.2.1", "@lodestar/utils": "^1.2.1", "@lodestar/validator": "^1.2.1", + "@lodestar/reqresp": "^0.1.0", "@multiformats/multiaddr": "^11.0.0", "@types/datastore-level": "^3.0.0", "buffer-xor": "^2.0.2", diff --git a/packages/beacon-node/src/network/events.ts b/packages/beacon-node/src/network/events.ts index 60d072432b1a..eb4cfdd4043e 100644 --- a/packages/beacon-node/src/network/events.ts +++ b/packages/beacon-node/src/network/events.ts @@ -2,7 +2,7 @@ import {EventEmitter} from "events"; import {PeerId} from "@libp2p/interface-peer-id"; import StrictEventEmitter from "strict-event-emitter-types"; import {allForks, phase0} from "@lodestar/types"; -import {RequestTypedContainer} from "./reqresp/index.js"; +import {RequestTypedContainer} from "@lodestar/reqresp"; export enum NetworkEvent { /** A relevant peer has connected or has been re-STATUS'd */ diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 0670c8b48e6a..3c66a1e5d7a4 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -2,11 +2,11 @@ import {Connection} from "@libp2p/interface-connection"; import {Multiaddr} from "@multiformats/multiaddr"; import {PeerId} from "@libp2p/interface-peer-id"; import {Discv5, ENR} from "@chainsafe/discv5"; +import {IReqResp} from "@lodestar/reqresp"; import {INetworkEventBus} from "./events.js"; import {Eth2Gossipsub} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {PeerAction} from "./peers/index.js"; -import {IReqResp} from "./reqresp/index.js"; import {IAttnetsService, ISubnetsService, CommitteeSubscription} from "./subnets/index.js"; export type PeerSearchOptions = { diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 2359a5071fc4..a9bc6489df35 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -9,11 +9,11 @@ import {ATTESTATION_SUBNET_COUNT, ForkName, SYNC_COMMITTEE_SUBNET_COUNT} from "@ import {Discv5, ENR} from "@chainsafe/discv5"; import {computeEpochAtSlot, computeTimeAtSlot} from "@lodestar/state-transition"; import {altair, Epoch} from "@lodestar/types"; +import {IReqResp, ReqRespOptions} from "@lodestar/reqresp"; import {IMetrics} from "../metrics/index.js"; import {ChainEvent, IBeaconChain, IBeaconClock} from "../chain/index.js"; import {INetworkOptions} from "./options.js"; import {INetwork} from "./interface.js"; -import {IReqResp, IReqRespOptions, ReqResp, ReqRespHandlers} from "./reqresp/index.js"; import {Eth2Gossipsub, getGossipHandlers, GossipHandlers, GossipType} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {FORK_EPOCH_LOOKAHEAD, getActiveForks} from "./forks.js"; @@ -23,6 +23,8 @@ import {INetworkEventBus, NetworkEventBus} from "./events.js"; import {AttnetsService, CommitteeSubscription, SyncnetsService} from "./subnets/index.js"; import {PeersData} from "./peers/peersData.js"; import {getConnectionsMap, isPublishToZeroPeersError} from "./util.js"; +import {getBeaconNodeReqResp} from "./reqresp/index.js"; +import {ReqRespHandlers} from "./reqresp/handlers/index.js"; interface INetworkModules { config: IBeaconConfig; @@ -56,8 +58,8 @@ export class Network implements INetwork { private subscribedForks = new Set(); - constructor(private readonly opts: INetworkOptions & IReqRespOptions, modules: INetworkModules) { - const {config, libp2p, logger, metrics, chain, reqRespHandlers, gossipHandlers, signal} = modules; + constructor(private readonly opts: INetworkOptions & Partial, modules: INetworkModules) { + const {config, libp2p, logger, metrics, chain, gossipHandlers, signal} = modules; this.libp2p = libp2p; this.logger = logger; this.config = config; @@ -71,19 +73,18 @@ export class Network implements INetwork { this.events = networkEventBus; this.metadata = metadata; this.peerRpcScores = peerRpcScores; - this.reqResp = new ReqResp( + this.reqResp = getBeaconNodeReqResp( { config, libp2p, - reqRespHandlers, - metadata, - peerRpcScores, logger, - networkEventBus, metrics, + metadata, + peerRpcScores, peersData: this.peersData, + networkEventBus, }, - opts + modules.reqRespHandlers ); this.gossip = new Eth2Gossipsub(opts, { diff --git a/packages/beacon-node/src/network/options.ts b/packages/beacon-node/src/network/options.ts index bbfd0d2036f2..837fb51a6332 100644 --- a/packages/beacon-node/src/network/options.ts +++ b/packages/beacon-node/src/network/options.ts @@ -1,10 +1,10 @@ import {ENR, IDiscv5DiscoveryInputOptions} from "@chainsafe/discv5"; +import {InboundRateLimiter, RateLimiterOptions} from "@lodestar/reqresp/rate_limiter"; import {Eth2GossipsubOpts} from "./gossip/gossipsub.js"; import {defaultGossipHandlerOpts, GossipHandlerOpts} from "./gossip/handlers/index.js"; import {PeerManagerOpts} from "./peers/index.js"; -import {defaultRateLimiterOpts, RateLimiterOpts} from "./reqresp/response/rateLimiter.js"; -export interface INetworkOptions extends PeerManagerOpts, RateLimiterOpts, GossipHandlerOpts, Eth2GossipsubOpts { +export interface INetworkOptions extends PeerManagerOpts, RateLimiterOptions, GossipHandlerOpts, Eth2GossipsubOpts { localMultiaddrs: string[]; bootMultiaddrs?: string[]; subscribeAllSubnets?: boolean; @@ -27,6 +27,6 @@ export const defaultNetworkOptions: INetworkOptions = { localMultiaddrs: ["/ip4/0.0.0.0/tcp/9000"], bootMultiaddrs: [], discv5: defaultDiscv5Options, - ...defaultRateLimiterOpts, + ...InboundRateLimiter.defaults, ...defaultGossipHandlerOpts, }; diff --git a/packages/beacon-node/src/network/peers/peerManager.ts b/packages/beacon-node/src/network/peers/peerManager.ts index da916f61eb02..e31924bfb249 100644 --- a/packages/beacon-node/src/network/peers/peerManager.ts +++ b/packages/beacon-node/src/network/peers/peerManager.ts @@ -7,11 +7,11 @@ import {SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params"; import {IBeaconConfig} from "@lodestar/config"; import {allForks, altair, phase0} from "@lodestar/types"; import {ILogger} from "@lodestar/utils"; +import {IReqResp, RequestTypedContainer, ReqRespMethod} from "@lodestar/reqresp"; import {IBeaconChain} from "../../chain/index.js"; import {GoodByeReasonCode, GOODBYE_KNOWN_CODES, Libp2pEvent} from "../../constants/index.js"; import {IMetrics} from "../../metrics/index.js"; import {NetworkEvent, INetworkEventBus} from "../events.js"; -import {IReqResp, ReqRespMethod, RequestTypedContainer} from "../reqresp/index.js"; import {getConnection, getConnectionsMap, prettyPrintPeerId} from "../util.js"; import {ISubnetsService} from "../subnets/index.js"; import {SubnetType} from "../metadata.js"; diff --git a/packages/beacon-node/src/network/peers/peersData.ts b/packages/beacon-node/src/network/peers/peersData.ts index cd3b90307ccc..3f57c5115f12 100644 --- a/packages/beacon-node/src/network/peers/peersData.ts +++ b/packages/beacon-node/src/network/peers/peersData.ts @@ -1,6 +1,6 @@ import {PeerId} from "@libp2p/interface-peer-id"; +import {Encoding} from "@lodestar/reqresp"; import {altair} from "@lodestar/types"; -import {Encoding} from "../reqresp/types.js"; import {ClientKind} from "./client.js"; type PeerIdStr = string; diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts new file mode 100644 index 000000000000..d21c4f970ac7 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -0,0 +1,151 @@ +import {fromHexString} from "@chainsafe/ssz"; +import {GENESIS_SLOT, MAX_REQUEST_BLOCKS} from "@lodestar/params"; +import {ContextBytesType, EncodedPayload, EncodedPayloadType, ResponseError, RespStatus} from "@lodestar/reqresp"; +import {allForks, phase0, Slot} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; +import {IBeaconDb} from "../../../db/index.js"; + +// TODO: Unit test + +export async function* onBeaconBlocksByRange( + request: phase0.BeaconBlocksByRangeRequest, + chain: IBeaconChain, + db: IBeaconDb +): AsyncIterable> { + const {startSlot, count} = validateBeaconBlocksByRangeRequest(request); + const lt = startSlot + count; + + // step < 1 was validated above + const archivedBlocksStream = getFinalizedBlocksByRange(startSlot, lt, db); + + // Inject recent blocks, not in the finalized cold DB + + let totalBlock = 0; + let slot = -1; + for await (const block of archivedBlocksStream) { + totalBlock++; + slot = block.slot; + yield { + type: EncodedPayloadType.bytes, + bytes: block.bytes, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.slot, + }, + }; + } + + slot = slot === -1 ? request.startSlot : slot + request.step; + const upperSlot = request.startSlot + request.count * request.step; + const slots = [] as number[]; + while (slot < upperSlot) { + slots.push(slot); + slot += request.step; + } + + const unfinalizedBlocks = await getUnfinalizedBlocksAtSlots(slots, {chain, db}); + for (const block of unfinalizedBlocks) { + if (block !== undefined) { + totalBlock++; + yield { + type: EncodedPayloadType.bytes, + bytes: block.bytes, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.slot, + }, + }; + } + } + if (totalBlock === 0) { + throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No block found"); + } +} + +async function* getFinalizedBlocksByRange( + gte: number, + lt: number, + db: IBeaconDb +): AsyncIterable<{slot: Slot; bytes: Uint8Array}> { + const binaryEntriesStream = db.blockArchive.binaryEntriesStream({ + gte, + lt, + }); + for await (const {key, value} of binaryEntriesStream) { + yield { + slot: db.blockArchive.decodeKey(key), + bytes: value, + }; + } +} + +/** Returned blocks have the same ordering as `slots` */ +async function getUnfinalizedBlocksAtSlots( + slots: Slot[], + {chain, db}: {chain: IBeaconChain; db: IBeaconDb} +): Promise<{slot: Slot; bytes: Uint8Array}[]> { + if (slots.length === 0) { + return []; + } + + const slotsSet = new Set(slots); + const minSlot = Math.min(...slots); // Slots must have length > 0 + const blockRootsPerSlot = new Map>(); + + // these blocks are on the same chain to head + for (const block of chain.forkChoice.iterateAncestorBlocks(chain.forkChoice.getHeadRoot())) { + if (block.slot < minSlot) { + break; + } else if (slotsSet.has(block.slot)) { + blockRootsPerSlot.set(block.slot, db.block.getBinary(fromHexString(block.blockRoot))); + } + } + + const unfinalizedBlocksOrNull = await Promise.all(slots.map((slot) => blockRootsPerSlot.get(slot))); + + const unfinalizedBlocks: {slot: Slot; bytes: Uint8Array}[] = []; + + for (let i = 0; i < unfinalizedBlocksOrNull.length; i++) { + const block = unfinalizedBlocksOrNull[i]; + if (block) { + unfinalizedBlocks.push({ + slot: slots[i], + bytes: block, + }); + } + } + + return unfinalizedBlocks; +} + +function validateBeaconBlocksByRangeRequest( + request: phase0.BeaconBlocksByRangeRequest +): phase0.BeaconBlocksByRangeRequest { + const {startSlot, step} = request; + let {count} = request; + if (step < 1) { + throw new ResponseError(RespStatus.INVALID_REQUEST, "step < 1"); + } + if (count < 1) { + throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1"); + } + // TODO: validate against MIN_EPOCHS_FOR_BLOCK_REQUESTS + if (startSlot < GENESIS_SLOT) { + throw new ResponseError(RespStatus.INVALID_REQUEST, "startSlot < genesis"); + } + + if (step > 1) { + // step > 1 is deprecated, see https://github.com/ethereum/consensus-specs/pull/2856 + count = 1; + } + + if (count > MAX_REQUEST_BLOCKS) { + count = MAX_REQUEST_BLOCKS; + } + + return { + startSlot, + step, + count, + }; +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts new file mode 100644 index 000000000000..53cd6fbbf5e6 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -0,0 +1,41 @@ +import {EncodedPayload, EncodedPayloadType, ContextBytesType} from "@lodestar/reqresp"; +import {getSlotFromBytes} from "@lodestar/reqresp/utils"; +import {allForks, phase0, Slot} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; +import {IBeaconDb} from "../../../db/index.js"; + +export async function* onBeaconBlocksByRoot( + requestBody: phase0.BeaconBlocksByRootRequest, + chain: IBeaconChain, + db: IBeaconDb +): AsyncIterable> { + for (const blockRoot of requestBody) { + const root = blockRoot; + const summary = chain.forkChoice.getBlock(root); + let blockBytes: Uint8Array | null = null; + + // finalized block has summary in forkchoice but it stays in blockArchive db + if (summary) { + blockBytes = await db.block.getBinary(root); + } + + let slot: Slot | undefined = undefined; + if (!blockBytes) { + const blockEntry = await db.blockArchive.getBinaryEntryByRoot(root); + if (blockEntry) { + slot = blockEntry.key; + blockBytes = blockEntry.value; + } + } + if (blockBytes) { + yield { + type: EncodedPayloadType.bytes, + bytes: blockBytes, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: slot ?? getSlotFromBytes(blockBytes), + }, + }; + } + } +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts new file mode 100644 index 000000000000..c6435161857f --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -0,0 +1,47 @@ +import {EncodedPayloadType, Handler} from "@lodestar/reqresp"; +import {phase0} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; +import {IBeaconDb} from "../../../db/index.js"; +import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js"; +import {onBeaconBlocksByRoot} from "./beaconBlocksByRoot.js"; +import {onLightClientBootstrap} from "./lightClientBootstrap.js"; +import {onLightClientFinalityUpdate} from "./lightClientFinalityUpdate.js"; +import {onLightClientOptimisticUpdate} from "./lightClientOptimisticUpdate.js"; +import {onLightClientUpdatesByRange} from "./lightClientUpdatesByRange.js"; + +export type ReqRespHandlers = ReturnType; +/** + * The ReqRespHandler module handles app-level requests / responses from other peers, + * fetching state from the chain and database as needed. + */ +export function getReqRespHandlers({ + db, + chain, +}: { + db: IBeaconDb; + chain: IBeaconChain; +}): {onStatus: Handler} { + return { + async *onStatus() { + yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; + }, + async *onBeaconBlocksByRange(req) { + yield* onBeaconBlocksByRange(req, chain, db); + }, + async *onBeaconBlocksByRoot(req) { + yield* onBeaconBlocksByRoot(req, chain, db); + }, + async *onLightClientBootstrap(req) { + yield* onLightClientBootstrap(req, chain); + }, + async *onLightClientUpdatesByRange(req) { + yield* onLightClientUpdatesByRange(req, chain); + }, + async *onLightClientFinalityUpdate() { + yield* onLightClientFinalityUpdate(chain); + }, + async *onLightClientOptimisticUpdate() { + yield* onLightClientOptimisticUpdate(chain); + }, + }; +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts new file mode 100644 index 000000000000..5ad51debea92 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientBootstrap.ts @@ -0,0 +1,28 @@ +import { + EncodedPayload, + EncodedPayloadType, + RespStatus, + ResponseError, + LightClientServerError, + LightClientServerErrorCode, +} from "@lodestar/reqresp"; +import {altair, Root} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; + +export async function* onLightClientBootstrap( + requestBody: Root, + chain: IBeaconChain +): AsyncIterable> { + try { + yield { + type: EncodedPayloadType.ssz, + data: await chain.lightClientServer.getBootstrap(requestBody), + }; + } catch (e) { + if ((e as LightClientServerError).type?.code === LightClientServerErrorCode.RESOURCE_UNAVAILABLE) { + throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, (e as Error).message); + } else { + throw new ResponseError(RespStatus.SERVER_ERROR, (e as Error).message); + } + } +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts new file mode 100644 index 000000000000..77bfab2cd16c --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientFinalityUpdate.ts @@ -0,0 +1,17 @@ +import {EncodedPayload, ResponseError, RespStatus, EncodedPayloadType} from "@lodestar/reqresp"; +import {altair} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; + +export async function* onLightClientFinalityUpdate( + chain: IBeaconChain +): AsyncIterable> { + const finalityUpdate = chain.lightClientServer.getFinalityUpdate(); + if (finalityUpdate === null) { + throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest finality update available"); + } else { + yield { + type: EncodedPayloadType.ssz, + data: finalityUpdate, + }; + } +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts new file mode 100644 index 000000000000..8bfae7d0f61f --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientOptimisticUpdate.ts @@ -0,0 +1,17 @@ +import {EncodedPayload, EncodedPayloadType, ResponseError, RespStatus} from "@lodestar/reqresp"; +import {altair} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; + +export async function* onLightClientOptimisticUpdate( + chain: IBeaconChain +): AsyncIterable> { + const optimisticUpdate = chain.lightClientServer.getOptimisticUpdate(); + if (optimisticUpdate === null) { + throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest optimistic update available"); + } else { + yield { + type: EncodedPayloadType.ssz, + data: optimisticUpdate, + }; + } +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts new file mode 100644 index 000000000000..d5ab43984a19 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/lightClientUpdatesByRange.ts @@ -0,0 +1,32 @@ +import {altair} from "@lodestar/types"; +import {MAX_REQUEST_LIGHT_CLIENT_UPDATES} from "@lodestar/params"; +import { + EncodedPayload, + EncodedPayloadType, + LightClientServerError, + LightClientServerErrorCode, + ResponseError, + RespStatus, +} from "@lodestar/reqresp"; +import {IBeaconChain} from "../../../chain/index.js"; + +export async function* onLightClientUpdatesByRange( + requestBody: altair.LightClientUpdatesByRange, + chain: IBeaconChain +): AsyncIterable> { + const count = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, requestBody.count); + for (let period = requestBody.startPeriod; period < requestBody.startPeriod + count; period++) { + try { + yield { + type: EncodedPayloadType.ssz, + data: await chain.lightClientServer.getUpdate(period), + }; + } catch (e) { + if ((e as LightClientServerError).type?.code === LightClientServerErrorCode.RESOURCE_UNAVAILABLE) { + throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, (e as Error).message); + } else { + throw new ResponseError(RespStatus.SERVER_ERROR, (e as Error).message); + } + } + } +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts b/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts new file mode 100644 index 000000000000..2c805ea7a336 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts @@ -0,0 +1,8 @@ +import {EncodedPayload, EncodedPayloadType} from "@lodestar/reqresp"; +import {phase0} from "@lodestar/types"; + +export async function* onStatus( + _request: phase0.BeaconBlocksByRangeRequest +): AsyncIterable> { + yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; +} diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts new file mode 100644 index 000000000000..549238d82900 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -0,0 +1,53 @@ +import { + getMetrics as getReqResMetrics, + Handler, + IReqResp, + MetricsRegister, + ReqResp, + ReqRespModules, +} from "@lodestar/reqresp"; +import messages from "@lodestar/reqresp/messages"; +import {InboundRateLimiter} from "@lodestar/reqresp/rate_limiter"; +import {phase0} from "@lodestar/types"; +import {IMetrics} from "../../metrics/index.js"; +import {ReqRespHandlers} from "./handlers/index.js"; + +export const getBeaconNodeReqResp = ( + modules: Omit & { + metrics: IMetrics | null; + }, + reqRespHandlers: ReqRespHandlers +): IReqResp => { + const metrics = modules.metrics + ? getReqResMetrics((modules.metrics as unknown) as MetricsRegister, { + version: "", + commit: "", + network: "", + }) + : null; + + const inboundRateLimiter = new InboundRateLimiter( + {}, + {logger: modules.logger, metrics, peerRpcScores: modules.peerRpcScores} + ); + + const reqRespModules = { + ...modules, + metrics, + inboundRateLimiter, + }; + + const reqresp = ReqResp.withDefaults(reqRespModules); + + reqresp.registerProtocol(messages.v1.Status(reqRespHandlers.onStatus, reqRespModules)); + reqresp.registerProtocol( + messages.v1.Ping( + async function* onPing() { + yield; + } as Handler, + reqRespModules + ) + ); + + return reqresp; +}; diff --git a/packages/beacon-node/src/node/nodejs.ts b/packages/beacon-node/src/node/nodejs.ts index 0647225302ab..8aa503e5b41f 100644 --- a/packages/beacon-node/src/node/nodejs.ts +++ b/packages/beacon-node/src/node/nodejs.ts @@ -10,7 +10,7 @@ import {BeaconStateAllForks} from "@lodestar/state-transition"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {IBeaconDb} from "../db/index.js"; -import {INetwork, Network, getReqRespHandlers} from "../network/index.js"; +import {INetwork, Network} from "../network/index.js"; import {BeaconSync, IBeaconSync} from "../sync/index.js"; import {BackfillSync} from "../sync/backfill/index.js"; import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js"; @@ -19,6 +19,7 @@ import {getApi, BeaconRestApiServer} from "../api/index.js"; import {initializeExecutionEngine, initializeExecutionBuilder} from "../execution/index.js"; import {initializeEth1ForBlockProduction} from "../eth1/index.js"; import {createLibp2pMetrics} from "../metrics/metrics/libp2p.js"; +import {getReqRespHandlers} from "../network/reqresp/handlers/index.js"; import {IBeaconNodeOptions} from "./options.js"; import {runNodeNotifier} from "./notifier.js"; From 831c8e657e1d7e22dfa3977f6e30c2291da99315 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 17 Nov 2022 02:36:13 +0100 Subject: [PATCH 06/23] Update the reqresp module handler structure --- packages/reqresp/src/ReqResp.ts | 81 ++++++++++--------- packages/reqresp/src/ReqRespProtocol.ts | 46 +++++------ packages/reqresp/src/interface.ts | 39 ++++----- packages/reqresp/src/messages/utils.ts | 7 +- .../src/messages/v1/BeaconBlocksByRange.ts | 7 +- .../src/messages/v1/BeaconBlocksByRoot.ts | 9 ++- packages/reqresp/src/messages/v1/Goodbye.ts | 4 +- .../src/messages/v1/LightClientBootstrap.ts | 10 ++- .../messages/v1/LightClientFinalityUpdate.ts | 10 ++- .../v1/LightClientOptimisticUpdate.ts | 9 ++- .../messages/v1/LightClientUpdatesByRange.ts | 7 +- packages/reqresp/src/messages/v1/Metadata.ts | 6 +- packages/reqresp/src/messages/v1/Ping.ts | 6 +- packages/reqresp/src/messages/v1/Status.ts | 9 ++- .../src/messages/v2/BeaconBlocksByRange.ts | 7 +- .../src/messages/v2/BeaconBlocksByRoot.ts | 7 +- packages/reqresp/src/messages/v2/Metadata.ts | 6 +- packages/reqresp/src/response/index.ts | 10 +-- packages/reqresp/src/types.ts | 23 +++--- 19 files changed, 164 insertions(+), 139 deletions(-) diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index cbd84f283c43..ee11bdd084b7 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -1,14 +1,14 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ForkName} from "@lodestar/params"; import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; -import {timeoutOptions} from "./constants.js"; -import {IReqResp, RateLimiter, ReqRespModules, RespStatus} from "./interface.js"; +import {IReqResp, RateLimiter, ReqRespHandlerContext, ReqRespModules, RespStatus} from "./interface.js"; +import {InboundRateLimiter, RateLimiterOptions} from "./rate_limiter/RateLimiter.js"; import {ReqRespProtocol} from "./ReqRespProtocol.js"; import {RequestError} from "./request/errors.js"; import {ResponseError} from "./response/errors.js"; import {onOutgoingReqRespError} from "./score.js"; -import {MetadataController, NetworkEvent} from "./sharedTypes.js"; -import {Method, ReqRespOptions, RequestTypedContainer, Version} from "./types.js"; +import {IPeerRpcScoreStore, MetadataController} from "./sharedTypes.js"; +import {Method, ReqRespOptions, Version} from "./types.js"; import {assertSequentialBlocksInRange} from "./utils/index.js"; /** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ @@ -24,49 +24,50 @@ export type ReqRespBlockResponse = { * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ -export class ReqResp extends ReqRespProtocol implements IReqResp { - private metadataController: MetadataController; +export abstract class ReqResp extends ReqRespProtocol implements IReqResp { private inboundRateLimiter: RateLimiter; + private peerRpcScores: IPeerRpcScoreStore; + private metadataController: MetadataController; - private constructor(modules: ReqRespModules, options: ReqRespOptions) { + constructor(modules: ReqRespModules, options: ReqRespOptions & Partial) { super(modules, options); - this.metadataController = modules.metadata; - this.inboundRateLimiter = modules.inboundRateLimiter; - } - - static withDefaults(modules: ReqRespModules, options?: Partial): IReqResp { - const optionsWithDefaults = { - ...timeoutOptions, - ...{ - onIncomingRequest: (modules: ReqRespModules, peerId: PeerId, method: Method) => { - if (method !== Method.Goodbye && !modules.inboundRateLimiter.allowRequest(peerId)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - }, - onOutgoingReqRespError: ( - modules: ReqRespModules, - peerId: PeerId, - method: Method, - error: RequestError - ): void => { - const peerAction = onOutgoingReqRespError(error, method); - if (peerAction !== null) { - modules.peerRpcScores.applyAction(peerId, peerAction, error.type.code); - } - }, - onIncomingRequestBody: (modules: ReqRespModules, req: RequestTypedContainer, peerId: PeerId): void => { - // Allow onRequest to return and close the stream - // For Goodbye there may be a race condition where the listener of `receivedGoodbye` - // disconnects in the same syncronous call, preventing the stream from ending cleanly - setTimeout(() => modules.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); - }, + this.peerRpcScores = modules.peerRpcScores; + this.metadataController = modules.metadataController; + this.inboundRateLimiter = new InboundRateLimiter(options, modules); + } + + protected onIncomingRequest(peerId: PeerId, method: Method): void { + if (method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + } + + protected onOutgoingRequestError(peerId: PeerId, method: Method, error: RequestError): void { + const peerAction = onOutgoingReqRespError(error, method); + if (peerAction !== null) { + this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); + } + } + + protected getContext(): ReqRespHandlerContext { + const context = super.getContext(); + return { + ...context, + modules: { + ...context.modules, + inboundRateLimiter: this.inboundRateLimiter, + metadataController: this.metadataController, }, - ...options, }; - - return new ReqResp(modules, optionsWithDefaults); } + // protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { + // // Allow onRequest to return and close the stream + // // For Goodbye there may be a race condition where the listener of `receivedGoodbye` + // // disconnects in the same syncronous call, preventing the stream from ending cleanly + // setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); + // } + async start(): Promise { await super.start(); this.inboundRateLimiter.start(); diff --git a/packages/reqresp/src/ReqRespProtocol.ts b/packages/reqresp/src/ReqRespProtocol.ts index 67ffacca1a02..bc5c9385ccde 100644 --- a/packages/reqresp/src/ReqRespProtocol.ts +++ b/packages/reqresp/src/ReqRespProtocol.ts @@ -2,26 +2,23 @@ import {setMaxListeners} from "node:events"; import {Connection, Stream} from "@libp2p/interface-connection"; import {PeerId} from "@libp2p/interface-peer-id"; import {Libp2p} from "libp2p"; -import {IBeaconConfig} from "@lodestar/config"; import {ILogger} from "@lodestar/utils"; +import {IBeaconConfig} from "@lodestar/config"; import {Metrics} from "./metrics.js"; import {RequestError, RequestErrorCode, sendRequest} from "./request/index.js"; import {handleRequest} from "./response/index.js"; -import {IPeerRpcScoreStore, MetadataController, PeersData} from "./sharedTypes.js"; -import {Encoding, ProtocolDefinition, ReqRespOptions} from "./types.js"; +import {PeersData} from "./sharedTypes.js"; +import {Encoding, Method, ProtocolDefinition, ReqRespOptions, RequestTypedContainer} from "./types.js"; import {formatProtocolID} from "./utils/index.js"; -import {RateLimiter, ReqRespHandlerContext} from "./interface.js"; +import {ReqRespHandlerProtocolContext} from "./interface.js"; type ProtocolID = string; export interface ReqRespProtocolModules { - config: IBeaconConfig; libp2p: Libp2p; peersData: PeersData; logger: ILogger; - peerRpcScores: IPeerRpcScoreStore; - inboundRateLimiter: RateLimiter; - metadata: MetadataController; + config: IBeaconConfig; metrics: Metrics | null; } @@ -31,7 +28,7 @@ export interface ReqRespProtocolModules { * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ -export class ReqRespProtocol { +export abstract class ReqRespProtocol { private libp2p: Libp2p; private readonly peersData: PeersData; private logger: ILogger; @@ -40,30 +37,20 @@ export class ReqRespProtocol { private reqCount = 0; private respCount = 0; private metrics: Metrics | null; + private config: IBeaconConfig; + /** `${protocolPrefix}/${method}/${version}/${encoding}` */ private readonly supportedProtocols = new Map(); - private readonly modules: ReqRespProtocolModules; constructor(modules: ReqRespProtocolModules, options: ReqRespOptions) { - this.modules = modules; this.options = options; + this.config = modules.config; this.libp2p = modules.libp2p; this.peersData = modules.peersData; this.logger = modules.logger; this.metrics = modules.metrics; } - getContext(): ReqRespHandlerContext { - return { - modules: this.modules, - eventsHandlers: { - onIncomingRequest: this.options.onIncomingRequest, - onIncomingRequestBody: this.options.onIncomingRequestBody, - onOutgoingReqRespError: this.options.onOutgoingReqRespError, - }, - }; - } - registerProtocol(protocol: ProtocolDefinition): void { const {method, version, encoding} = protocol; const protocolID = formatProtocolID(method, version, encoding); @@ -134,7 +121,7 @@ export class ReqRespProtocol { this.metrics?.dialErrors.inc(); } - this.onOutgoingReqRespError(peerId, method, e); + this.onOutgoingRequestError(peerId, method as Method, e); } throw e; @@ -177,11 +164,14 @@ export class ReqRespProtocol { }; } - protected onIncomingRequest(_peerId: PeerId, _method: string): void { - // Override + protected getContext(): Context { + return { + modules: {config: this.config, logger: this.logger, metrics: this.metrics, peersData: this.peersData}, + eventHandlers: {onIncomingRequestBody: this.onIncomingRequestBody}, + } as Context; } - protected onOutgoingReqRespError(_peerId: PeerId, _method: string, _error: RequestError): void { - // Override - } + protected abstract onIncomingRequestBody(_req: RequestTypedContainer, _peerId: PeerId): void; + protected abstract onOutgoingRequestError(_peerId: PeerId, _method: Method, _error: RequestError): void; + protected abstract onIncomingRequest(_peerId: PeerId, _method: Method): void; } diff --git a/packages/reqresp/src/interface.ts b/packages/reqresp/src/interface.ts index e9103f0a8f14..fb57bb0e1b34 100644 --- a/packages/reqresp/src/interface.ts +++ b/packages/reqresp/src/interface.ts @@ -1,14 +1,9 @@ -import {Libp2p} from "libp2p"; import {PeerId} from "@libp2p/interface-peer-id"; import {ForkName} from "@lodestar/params"; -import {IBeaconConfig} from "@lodestar/config"; import {allForks, altair, phase0} from "@lodestar/types"; -import {ILogger} from "@lodestar/utils"; -import {INetworkEventBus, IPeerRpcScoreStore, MetadataController, PeersData} from "./sharedTypes.js"; -import {Metrics} from "./metrics.js"; -import {Method, ProtocolDefinition, RequestTypedContainer} from "./types.js"; -import {RequestError} from "./request/errors.js"; import {ReqRespProtocolModules} from "./ReqRespProtocol.js"; +import {IPeerRpcScoreStore, MetadataController} from "./sharedTypes.js"; +import {ProtocolDefinition, RequestTypedContainer} from "./types.js"; export interface IReqResp { start(): void; @@ -30,27 +25,23 @@ export interface IReqResp { registerProtocol(protocol: ProtocolDefinition): void; } -export interface ReqRespEventsHandlers { - onIncomingRequestBody(modules: ReqRespProtocolModules, req: RequestTypedContainer, peerId: PeerId): void; - onOutgoingReqRespError(modules: ReqRespProtocolModules, peerId: PeerId, method: Method, error: RequestError): void; - onIncomingRequest(modules: ReqRespProtocolModules, peerId: PeerId, method: Method): void; +export interface ReqRespModules extends ReqRespProtocolModules { + peerRpcScores: IPeerRpcScoreStore; + metadataController: MetadataController; } -export interface ReqRespModules { - config: IBeaconConfig; - libp2p: Libp2p; - peersData: PeersData; - logger: ILogger; - metadata: MetadataController; - peerRpcScores: IPeerRpcScoreStore; - networkEventBus: INetworkEventBus; - metrics: Metrics | null; - inboundRateLimiter: RateLimiter; +export interface ReqRespHandlerProtocolContext { + modules: Omit; + eventHandlers: { + onIncomingRequestBody(_req: RequestTypedContainer, _peerId: PeerId): void; + }; } -export interface ReqRespHandlerContext { - modules: ReqRespProtocolModules; - eventsHandlers: ReqRespEventsHandlers; +export interface ReqRespHandlerContext extends ReqRespHandlerProtocolContext { + modules: Omit & { + inboundRateLimiter: RateLimiter; + metadataController: MetadataController; + }; } /** diff --git a/packages/reqresp/src/messages/utils.ts b/packages/reqresp/src/messages/utils.ts index a889c2925ce5..3669bb1e7053 100644 --- a/packages/reqresp/src/messages/utils.ts +++ b/packages/reqresp/src/messages/utils.ts @@ -1,10 +1,10 @@ import {ForkName} from "@lodestar/params"; -import {ReqRespModules} from "../interface.js"; +import {ReqRespHandlerContext} from "../interface.js"; import {ContextBytesType, ContextBytesFactory} from "../types.js"; export function getContextBytesLightclient( forkFromResponse: (response: T) => ForkName, - modules: ReqRespModules + modules: ReqRespHandlerContext["modules"] ): ContextBytesFactory { return { type: ContextBytesType.ForkDigest, @@ -12,3 +12,6 @@ export function getContextBytesLightclient( forkFromResponse, }; } + +export const getHandlerRequiredErrorFor = (method: string): Error => + new Error(`Handler is required for method "${method}."`); diff --git a/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts index 6ed9960ecde3..fe2faeb1918e 100644 --- a/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts +++ b/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts @@ -2,12 +2,17 @@ import {allForks, phase0, ssz} from "@lodestar/types"; import {RespStatus} from "../../interface.js"; import {ResponseError} from "../../response/errors.js"; import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const BeaconBlocksByRange: ProtocolDefinitionGenerator< phase0.BeaconBlocksByRangeRequest, allForks.SignedBeaconBlock -> = (handler) => { +> = (_modules, handler) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRange); + } + return { method: Method.BeaconBlocksByRange, version: Version.V1, diff --git a/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts index 06669790eeb1..da19028d5ffb 100644 --- a/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts +++ b/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts @@ -3,14 +3,19 @@ import {toHex} from "@lodestar/utils"; import {RespStatus} from "../../interface.js"; import {ResponseError} from "../../response/errors.js"; import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const BeaconBlocksByRoot: ProtocolDefinitionGenerator< phase0.BeaconBlocksByRootRequest, allForks.SignedBeaconBlock -> = (handler) => { +> = (_modules, handler) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRoot); + } + return { - method: Method.BeaconBlocksByRange, + method: Method.BeaconBlocksByRoot, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, handler: async function* beaconBlocksByRootHandler(context, req, peerId) { diff --git a/packages/reqresp/src/messages/v1/Goodbye.ts b/packages/reqresp/src/messages/v1/Goodbye.ts index f31306fdcc03..beff4fa20d47 100644 --- a/packages/reqresp/src/messages/v1/Goodbye.ts +++ b/packages/reqresp/src/messages/v1/Goodbye.ts @@ -15,9 +15,9 @@ export const Goodbye: ProtocolDefinitionGenerator ssz.phase0.Goodbye, responseType: () => ssz.phase0.Goodbye, diff --git a/packages/reqresp/src/messages/v1/LightClientBootstrap.ts b/packages/reqresp/src/messages/v1/LightClientBootstrap.ts index f0026ce38522..e7448e913da4 100644 --- a/packages/reqresp/src/messages/v1/LightClientBootstrap.ts +++ b/packages/reqresp/src/messages/v1/LightClientBootstrap.ts @@ -1,13 +1,17 @@ import {altair, Root, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient} from "../utils.js"; +import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientBootstrap: ProtocolDefinitionGenerator = ( - handler, - modules + modules, + handler ) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.LightClientBootstrap); + } + return { method: Method.LightClientBootstrap, version: Version.V1, diff --git a/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts b/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts index c92f71036001..ad033e8df346 100644 --- a/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts +++ b/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts @@ -1,12 +1,16 @@ import {altair, ssz} from "@lodestar/types"; import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient} from "../utils.js"; +import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientFinalityUpdate: ProtocolDefinitionGenerator = ( - handler, - modules + modules, + handler ) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.LightClientFinalityUpdate); + } + return { method: Method.LightClientFinalityUpdate, version: Version.V1, diff --git a/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts b/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts index b29208978e14..614d12be87fc 100644 --- a/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts +++ b/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts @@ -1,12 +1,15 @@ import {altair, ssz} from "@lodestar/types"; import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient} from "../utils.js"; +import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientOptimisticUpdate: ProtocolDefinitionGenerator = ( - handler, - modules + modules, + handler ) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.LightClientOptimisticUpdate); + } return { method: Method.LightClientOptimisticUpdate, version: Version.V1, diff --git a/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts b/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts index 0d1bd593a625..74e05bdb4eb6 100644 --- a/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts +++ b/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts @@ -1,12 +1,15 @@ import {altair, ssz} from "@lodestar/types"; import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient} from "../utils.js"; +import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientUpdatesByRange: ProtocolDefinitionGenerator< altair.LightClientUpdatesByRange, altair.LightClientUpdate -> = (handler, modules) => { +> = (modules, handler) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.LightClientUpdatesByRange); + } return { method: Method.LightClientUpdatesByRange, version: Version.V1, diff --git a/packages/reqresp/src/messages/v1/Metadata.ts b/packages/reqresp/src/messages/v1/Metadata.ts index 0337a7d42c26..e5ddda0df86f 100644 --- a/packages/reqresp/src/messages/v1/Metadata.ts +++ b/packages/reqresp/src/messages/v1/Metadata.ts @@ -9,15 +9,15 @@ import { } from "../../types.js"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const Metadata: ProtocolDefinitionGenerator = (_handler, modules) => { +export const Metadata: ProtocolDefinitionGenerator = (modules) => { return { method: Method.Metadata, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, handler: async function* metadataHandler(context, req, peerId) { - context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Metadata, body: req}, peerId); + context.eventHandlers.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); - yield {type: EncodedPayloadType.ssz, data: modules.metadata.json}; + yield {type: EncodedPayloadType.ssz, data: modules.metadataController.json}; }, requestType: () => null, responseType: () => ssz.phase0.Metadata, diff --git a/packages/reqresp/src/messages/v1/Ping.ts b/packages/reqresp/src/messages/v1/Ping.ts index 237ef7afd90f..009aa5dca042 100644 --- a/packages/reqresp/src/messages/v1/Ping.ts +++ b/packages/reqresp/src/messages/v1/Ping.ts @@ -9,15 +9,15 @@ import { } from "../../types.js"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const Ping: ProtocolDefinitionGenerator = (_handler, modules) => { +export const Ping: ProtocolDefinitionGenerator = (modules) => { return { method: Method.Status, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, handler: async function* pingHandler(context, req, peerId) { - context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Ping, body: req}, peerId); + context.eventHandlers.onIncomingRequestBody({method: Method.Ping, body: req}, peerId); - yield {type: EncodedPayloadType.ssz, data: modules.metadata.seqNumber}; + yield {type: EncodedPayloadType.ssz, data: modules.metadataController.seqNumber}; }, requestType: () => ssz.phase0.Ping, responseType: () => ssz.phase0.Ping, diff --git a/packages/reqresp/src/messages/v1/Status.ts b/packages/reqresp/src/messages/v1/Status.ts index 055cf2dc9c14..0d204702a9c8 100644 --- a/packages/reqresp/src/messages/v1/Status.ts +++ b/packages/reqresp/src/messages/v1/Status.ts @@ -1,14 +1,19 @@ import {phase0, ssz} from "@lodestar/types"; import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const Status: ProtocolDefinitionGenerator = (handler) => { +export const Status: ProtocolDefinitionGenerator = (_modules, handler) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.Status); + } + return { method: Method.Status, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, handler: async function* statusHandler(context, req, peerId) { - context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Status, body: req}, peerId); + context.eventHandlers.onIncomingRequestBody({method: Method.Status, body: req}, peerId); yield* handler(req, peerId); }, diff --git a/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts index 16012c2ef543..0df2ffdaadb6 100644 --- a/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts +++ b/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts @@ -2,12 +2,17 @@ import {allForks, phase0, ssz} from "@lodestar/types"; import {RespStatus} from "../../interface.js"; import {ResponseError} from "../../response/errors.js"; import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const BeaconBlocksByRangeV2: ProtocolDefinitionGenerator< phase0.BeaconBlocksByRangeRequest, allForks.SignedBeaconBlock -> = (handler, modules) => { +> = (modules, handler) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRange); + } + return { method: Method.BeaconBlocksByRange, version: Version.V2, diff --git a/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts index 1afde6e5b8f5..aacef8789177 100644 --- a/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts +++ b/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts @@ -3,12 +3,17 @@ import {toHex} from "@lodestar/utils"; import {RespStatus} from "../../interface.js"; import {ResponseError} from "../../response/errors.js"; import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; +import {getHandlerRequiredErrorFor} from "../utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const BeaconBlocksByRootV2: ProtocolDefinitionGenerator< phase0.BeaconBlocksByRootRequest, allForks.SignedBeaconBlock -> = (handler, modules) => { +> = (modules, handler) => { + if (!handler) { + throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRoot); + } + return { method: Method.BeaconBlocksByRoot, version: Version.V2, diff --git a/packages/reqresp/src/messages/v2/Metadata.ts b/packages/reqresp/src/messages/v2/Metadata.ts index 2f755ad80bfc..4dfde9756b25 100644 --- a/packages/reqresp/src/messages/v2/Metadata.ts +++ b/packages/reqresp/src/messages/v2/Metadata.ts @@ -9,15 +9,15 @@ import { } from "../../types.js"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const MetadataV2: ProtocolDefinitionGenerator = (_handler, modules) => { +export const MetadataV2: ProtocolDefinitionGenerator = (modules) => { return { method: Method.Metadata, version: Version.V2, encoding: Encoding.SSZ_SNAPPY, handler: async function* metadataV2Handler(context, req, peerId) { - context.eventsHandlers.onIncomingRequestBody(context.modules, {method: Method.Metadata, body: req}, peerId); + context.eventHandlers.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); - yield {type: EncodedPayloadType.ssz, data: modules.metadata.json}; + yield {type: EncodedPayloadType.ssz, data: modules.metadataController.json}; }, requestType: () => null, responseType: () => ssz.altair.Metadata, diff --git a/packages/reqresp/src/response/index.ts b/packages/reqresp/src/response/index.ts index 302a2f24137b..57e93467ba8a 100644 --- a/packages/reqresp/src/response/index.ts +++ b/packages/reqresp/src/response/index.ts @@ -8,13 +8,13 @@ import {prettyPrintPeerId} from "../utils/index.js"; import {ProtocolDefinition} from "../types.js"; import {requestDecode} from "../encoders/requestDecode.js"; import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js"; -import {ReqRespHandlerContext, RespStatus} from "../interface.js"; +import {RespStatus} from "../interface.js"; import {ResponseError} from "./errors.js"; export {ResponseError}; -export interface HandleRequestOpts { - context: ReqRespHandlerContext; +export interface HandleRequestOpts { + context: Context; logger: ILogger; stream: Stream; peerId: PeerId; @@ -35,7 +35,7 @@ export interface HandleRequestOpts { * 4a. Encode and write `` to peer * 4b. On error, encode and write an error `` and stop */ -export async function handleRequest({ +export async function handleRequest({ context, logger, stream, @@ -44,7 +44,7 @@ export async function handleRequest({ signal, requestId = 0, peerClient = "unknown", -}: HandleRequestOpts): Promise { +}: HandleRequestOpts): Promise { const logCtx = {method: protocol.method, client: peerClient, peer: prettyPrintPeerId(peerId), requestId}; let responseError: Error | null = null; diff --git a/packages/reqresp/src/types.ts b/packages/reqresp/src/types.ts index b3416d4f3c71..b245dc5dd0c5 100644 --- a/packages/reqresp/src/types.ts +++ b/packages/reqresp/src/types.ts @@ -4,8 +4,8 @@ import {IForkConfig, IForkDigestContext} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; import {phase0, Slot} from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; -import {ReqRespEventsHandlers, ReqRespHandlerContext, ReqRespModules} from "./interface.js"; import {timeoutOptions} from "./constants.js"; +import {ReqRespHandlerContext, ReqRespHandlerProtocolContext} from "./interface.js"; export enum EncodedPayloadType { ssz, @@ -23,16 +23,16 @@ export type EncodedPayload = contextBytes: ContextBytes; }; -export type HandlerWithContext = ( - context: ReqRespHandlerContext, +export type ReqRespHandlerWithContext = ( + context: Context, req: Req, peerId: PeerId ) => AsyncIterable>; -export type Handler = (req: Req, peerId: PeerId) => AsyncIterable>; +export type ReqRespHandler = (req: Req, peerId: PeerId) => AsyncIterable>; -export interface ProtocolDefinition extends Protocol { - handler: HandlerWithContext; +export interface ProtocolDefinition extends Protocol { + handler: ReqRespHandlerWithContext; // eslint-disable-next-line @typescript-eslint/no-explicit-any requestType: (fork: ForkName) => Type | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -42,10 +42,11 @@ export interface ProtocolDefinition extends Proto isSingleResponse: boolean; } -export type ProtocolDefinitionGenerator = ( - handler: Handler, - modules: ReqRespModules -) => ProtocolDefinition; +export type ProtocolDefinitionGenerator< + Req, + Res, + Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext +> = (modules: Context["modules"], handler?: ReqRespHandler) => ProtocolDefinition; export const protocolPrefix = "/eth2/beacon_chain/req"; @@ -122,7 +123,7 @@ export enum ContextBytesType { ForkDigest, } -export type ReqRespOptions = typeof timeoutOptions & ReqRespEventsHandlers; +export type ReqRespOptions = typeof timeoutOptions; export enum LightClientServerErrorCode { RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE", From d2c3d79d591e355ffd7ee928305ffdddefe51038 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 17 Nov 2022 03:48:36 +0100 Subject: [PATCH 07/23] Update reqresp handlers for beacon-node --- packages/beacon-node/src/network/network.ts | 2 +- .../src/network/reqresp/BeaconNodeReqResp.ts | 24 +++++++++ .../src/network/reqresp/handlers/index.ts | 17 +++++-- .../src/network/reqresp/handlers/onStatus.ts | 5 +- .../beacon-node/src/network/reqresp/index.ts | 49 +++++++++---------- packages/reqresp/src/ReqResp.ts | 25 ++++++---- packages/reqresp/src/ReqRespProtocol.ts | 2 +- packages/reqresp/src/interface.ts | 2 +- packages/reqresp/src/messages/utils.ts | 4 +- packages/reqresp/src/response/index.ts | 7 +-- packages/reqresp/src/types.ts | 26 +++++++--- 11 files changed, 105 insertions(+), 58 deletions(-) create mode 100644 packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index a9bc6489df35..56e6617211e1 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -79,7 +79,7 @@ export class Network implements INetwork { libp2p, logger, metrics, - metadata, + metadataController: metadata, peerRpcScores, peersData: this.peersData, networkEventBus, diff --git a/packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts b/packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts new file mode 100644 index 000000000000..11d457f75563 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts @@ -0,0 +1,24 @@ +import {PeerId} from "@libp2p/interface-peer-id"; +import {ReqResp, ReqRespModules, ReqRespOptions, RequestTypedContainer} from "@lodestar/reqresp"; +import {RateLimiterOptions} from "@lodestar/reqresp/rate_limiter"; +import {INetworkEventBus, NetworkEvent} from "../events.js"; + +export interface BeaconNodeReqRespModules extends ReqRespModules { + networkEventBus: INetworkEventBus; +} + +export class BeaconNodeReqResp extends ReqResp { + private networkEventBus: INetworkEventBus; + + constructor(modules: BeaconNodeReqRespModules, options?: Partial & Partial) { + super(modules, options); + this.networkEventBus = modules.networkEventBus; + } + + protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { + // Allow onRequest to return and close the stream + // For Goodbye there may be a race condition where the listener of `receivedGoodbye` + // disconnects in the same syncronous call, preventing the stream from ending cleanly + setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); + } +} diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index c6435161857f..33f9d99a8998 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -1,5 +1,5 @@ -import {EncodedPayloadType, Handler} from "@lodestar/reqresp"; -import {phase0} from "@lodestar/types"; +import {HandlerTypeFromMessage} from "@lodestar/reqresp"; +import messages from "@lodestar/reqresp/messages"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js"; @@ -8,6 +8,7 @@ import {onLightClientBootstrap} from "./lightClientBootstrap.js"; import {onLightClientFinalityUpdate} from "./lightClientFinalityUpdate.js"; import {onLightClientOptimisticUpdate} from "./lightClientOptimisticUpdate.js"; import {onLightClientUpdatesByRange} from "./lightClientUpdatesByRange.js"; +import {onStatus} from "./onStatus.js"; export type ReqRespHandlers = ReturnType; /** @@ -20,10 +21,18 @@ export function getReqRespHandlers({ }: { db: IBeaconDb; chain: IBeaconChain; -}): {onStatus: Handler} { +}): { + onStatus: HandlerTypeFromMessage; + onBeaconBlocksByRange: HandlerTypeFromMessage; + onBeaconBlocksByRoot: HandlerTypeFromMessage; + onLightClientBootstrap: HandlerTypeFromMessage; + onLightClientUpdatesByRange: HandlerTypeFromMessage; + onLightClientFinalityUpdate: HandlerTypeFromMessage; + onLightClientOptimisticUpdate: HandlerTypeFromMessage; +} { return { async *onStatus() { - yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; + yield* onStatus(chain); }, async *onBeaconBlocksByRange(req) { yield* onBeaconBlocksByRange(req, chain, db); diff --git a/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts b/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts index 2c805ea7a336..e05f6166c3b0 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts @@ -1,8 +1,7 @@ import {EncodedPayload, EncodedPayloadType} from "@lodestar/reqresp"; import {phase0} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; -export async function* onStatus( - _request: phase0.BeaconBlocksByRangeRequest -): AsyncIterable> { +export async function* onStatus(chain: IBeaconChain): AsyncIterable> { yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; } diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index 549238d82900..ceb9bb33d92d 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -1,19 +1,11 @@ -import { - getMetrics as getReqResMetrics, - Handler, - IReqResp, - MetricsRegister, - ReqResp, - ReqRespModules, -} from "@lodestar/reqresp"; +import {getMetrics as getReqResMetrics, IReqResp, MetricsRegister} from "@lodestar/reqresp"; import messages from "@lodestar/reqresp/messages"; -import {InboundRateLimiter} from "@lodestar/reqresp/rate_limiter"; -import {phase0} from "@lodestar/types"; import {IMetrics} from "../../metrics/index.js"; +import {BeaconNodeReqResp, BeaconNodeReqRespModules} from "./BeaconNodeReqResp.js"; import {ReqRespHandlers} from "./handlers/index.js"; export const getBeaconNodeReqResp = ( - modules: Omit & { + modules: Omit & { metrics: IMetrics | null; }, reqRespHandlers: ReqRespHandlers @@ -26,28 +18,33 @@ export const getBeaconNodeReqResp = ( }) : null; - const inboundRateLimiter = new InboundRateLimiter( - {}, - {logger: modules.logger, metrics, peerRpcScores: modules.peerRpcScores} - ); - const reqRespModules = { ...modules, metrics, - inboundRateLimiter, - }; + } as BeaconNodeReqRespModules; - const reqresp = ReqResp.withDefaults(reqRespModules); + const reqresp = new BeaconNodeReqResp(reqRespModules); - reqresp.registerProtocol(messages.v1.Status(reqRespHandlers.onStatus, reqRespModules)); + reqresp.registerProtocol(messages.v1.Ping(reqRespModules)); + reqresp.registerProtocol(messages.v1.Status(reqRespModules, reqRespHandlers.onStatus)); + reqresp.registerProtocol(messages.v1.Metadata(reqRespModules)); + reqresp.registerProtocol(messages.v1.Goodbye(reqRespModules)); + reqresp.registerProtocol(messages.v1.BeaconBlocksByRange(reqRespModules, reqRespHandlers.onBeaconBlocksByRange)); + reqresp.registerProtocol(messages.v1.BeaconBlocksByRoot(reqRespModules, reqRespHandlers.onBeaconBlocksByRoot)); + reqresp.registerProtocol(messages.v1.LightClientBootstrap(reqRespModules, reqRespHandlers.onLightClientBootstrap)); + reqresp.registerProtocol( + messages.v1.LightClientFinalityUpdate(reqRespModules, reqRespHandlers.onLightClientFinalityUpdate) + ); reqresp.registerProtocol( - messages.v1.Ping( - async function* onPing() { - yield; - } as Handler, - reqRespModules - ) + messages.v1.LightClientOptimisticUpdate(reqRespModules, reqRespHandlers.onLightClientOptimisticUpdate) ); + reqresp.registerProtocol( + messages.v1.LightClientUpdatesByRange(reqRespModules, reqRespHandlers.onLightClientUpdatesByRange) + ); + + reqresp.registerProtocol(messages.v2.Metadata(reqRespModules)); + reqresp.registerProtocol(messages.v2.BeaconBlocksByRange(reqRespModules, reqRespHandlers.onBeaconBlocksByRange)); + reqresp.registerProtocol(messages.v2.BeaconBlocksByRoot(reqRespModules, reqRespHandlers.onBeaconBlocksByRoot)); return reqresp; }; diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index ee11bdd084b7..7d0c4973ffbd 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -1,6 +1,7 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ForkName} from "@lodestar/params"; import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; +import {DIAL_TIMEOUT, REQUEST_TIMEOUT, RESP_TIMEOUT, TTFB_TIMEOUT} from "./constants.js"; import {IReqResp, RateLimiter, ReqRespHandlerContext, ReqRespModules, RespStatus} from "./interface.js"; import {InboundRateLimiter, RateLimiterOptions} from "./rate_limiter/RateLimiter.js"; import {ReqRespProtocol} from "./ReqRespProtocol.js"; @@ -18,6 +19,17 @@ export type ReqRespBlockResponse = { slot: Slot; }; +export const defaultReqRespOptions: ReqRespOptions = { + // eslint-disable-next-line @typescript-eslint/naming-convention + TTFB_TIMEOUT, + // eslint-disable-next-line @typescript-eslint/naming-convention + RESP_TIMEOUT, + // eslint-disable-next-line @typescript-eslint/naming-convention + REQUEST_TIMEOUT, + // eslint-disable-next-line @typescript-eslint/naming-convention + DIAL_TIMEOUT, +}; + /** * Implementation of Ethereum Consensus p2p Req/Resp domain. * For the spec that this code is based on, see: @@ -29,11 +41,11 @@ export abstract class ReqResp extends ReqRespProtocol imp private peerRpcScores: IPeerRpcScoreStore; private metadataController: MetadataController; - constructor(modules: ReqRespModules, options: ReqRespOptions & Partial) { - super(modules, options); + constructor(modules: ReqRespModules, options?: Partial & Partial) { + super(modules, {...defaultReqRespOptions, ...options}); this.peerRpcScores = modules.peerRpcScores; this.metadataController = modules.metadataController; - this.inboundRateLimiter = new InboundRateLimiter(options, modules); + this.inboundRateLimiter = new InboundRateLimiter({...InboundRateLimiter.defaults, ...options}, modules); } protected onIncomingRequest(peerId: PeerId, method: Method): void { @@ -61,13 +73,6 @@ export abstract class ReqResp extends ReqRespProtocol imp }; } - // protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { - // // Allow onRequest to return and close the stream - // // For Goodbye there may be a race condition where the listener of `receivedGoodbye` - // // disconnects in the same syncronous call, preventing the stream from ending cleanly - // setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); - // } - async start(): Promise { await super.start(); this.inboundRateLimiter.start(); diff --git a/packages/reqresp/src/ReqRespProtocol.ts b/packages/reqresp/src/ReqRespProtocol.ts index bc5c9385ccde..c5a73592e5cd 100644 --- a/packages/reqresp/src/ReqRespProtocol.ts +++ b/packages/reqresp/src/ReqRespProtocol.ts @@ -142,7 +142,7 @@ export abstract class ReqRespProtocol({ context: this.getContext(), logger: this.logger, stream, diff --git a/packages/reqresp/src/interface.ts b/packages/reqresp/src/interface.ts index fb57bb0e1b34..df2a441c5dde 100644 --- a/packages/reqresp/src/interface.ts +++ b/packages/reqresp/src/interface.ts @@ -38,7 +38,7 @@ export interface ReqRespHandlerProtocolContext { } export interface ReqRespHandlerContext extends ReqRespHandlerProtocolContext { - modules: Omit & { + modules: ReqRespHandlerProtocolContext["modules"] & { inboundRateLimiter: RateLimiter; metadataController: MetadataController; }; diff --git a/packages/reqresp/src/messages/utils.ts b/packages/reqresp/src/messages/utils.ts index 3669bb1e7053..4ad874501057 100644 --- a/packages/reqresp/src/messages/utils.ts +++ b/packages/reqresp/src/messages/utils.ts @@ -1,10 +1,10 @@ +import {IBeaconConfig} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; -import {ReqRespHandlerContext} from "../interface.js"; import {ContextBytesType, ContextBytesFactory} from "../types.js"; export function getContextBytesLightclient( forkFromResponse: (response: T) => ForkName, - modules: ReqRespHandlerContext["modules"] + modules: {config: IBeaconConfig} ): ContextBytesFactory { return { type: ContextBytesType.ForkDigest, diff --git a/packages/reqresp/src/response/index.ts b/packages/reqresp/src/response/index.ts index 57e93467ba8a..f01ffce45d18 100644 --- a/packages/reqresp/src/response/index.ts +++ b/packages/reqresp/src/response/index.ts @@ -8,7 +8,7 @@ import {prettyPrintPeerId} from "../utils/index.js"; import {ProtocolDefinition} from "../types.js"; import {requestDecode} from "../encoders/requestDecode.js"; import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js"; -import {RespStatus} from "../interface.js"; +import {ReqRespHandlerContext, ReqRespHandlerProtocolContext, RespStatus} from "../interface.js"; import {ResponseError} from "./errors.js"; export {ResponseError}; @@ -35,7 +35,7 @@ export interface HandleRequestOpts { * 4a. Encode and write `` to peer * 4b. On error, encode and write an error `` and stop */ -export async function handleRequest({ +export async function handleRequest({ context, logger, stream, @@ -69,7 +69,8 @@ export async function handleRequest({ logger.debug("Resp received request", {...logCtx, body: protocol.renderRequestBody?.(requestBody)}); yield* pipe( - protocol.handler(context, requestBody, peerId), + // TODO: Debug the reason for type conversion here + protocol.handler((context as unknown) as ReqRespHandlerContext, requestBody, peerId), // NOTE: Do not log the resp chunk contents, logs get extremely cluttered // Note: Not logging on each chunk since after 1 year it hasn't add any value when debugging // onChunk(() => logger.debug("Resp sending chunk", logCtx)), diff --git a/packages/reqresp/src/types.ts b/packages/reqresp/src/types.ts index b245dc5dd0c5..0098d303e9c2 100644 --- a/packages/reqresp/src/types.ts +++ b/packages/reqresp/src/types.ts @@ -23,15 +23,19 @@ export type EncodedPayload = contextBytes: ContextBytes; }; -export type ReqRespHandlerWithContext = ( - context: Context, - req: Req, - peerId: PeerId -) => AsyncIterable>; +export type ReqRespHandlerWithContext< + Req, + Resp, + Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext +> = (context: Context, req: Req, peerId: PeerId) => AsyncIterable>; export type ReqRespHandler = (req: Req, peerId: PeerId) => AsyncIterable>; -export interface ProtocolDefinition extends Protocol { +export interface ProtocolDefinition< + Req = unknown, + Resp = unknown, + Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext +> extends Protocol { handler: ReqRespHandlerWithContext; // eslint-disable-next-line @typescript-eslint/no-explicit-any requestType: (fork: ForkName) => Type | null; @@ -46,7 +50,15 @@ export type ProtocolDefinitionGenerator< Req, Res, Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext -> = (modules: Context["modules"], handler?: ReqRespHandler) => ProtocolDefinition; +> = ( + // "inboundRateLimiter" is available only on handler context not on generator + modules: Omit, + handler?: ReqRespHandler +) => ProtocolDefinition; + +export type HandlerTypeFromMessage = T extends ProtocolDefinitionGenerator + ? ReqRespHandler + : never; export const protocolPrefix = "/eth2/beacon_chain/req"; From 38b922598751bd75fd94431792d7517077a4b36f Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 18 Nov 2022 13:33:26 +0100 Subject: [PATCH 08/23] Review abstractions --- .../src/network/reqresp/BeaconNodeReqResp.ts | 24 -- .../reqresp/handlers/beaconBlocksByRoot.ts | 2 +- .../src/network/reqresp/handlers/index.ts | 14 +- .../beacon-node/src/network/reqresp/index.ts | 311 +++++++++++++++--- .../src/network/reqresp/interface.ts | 69 ++++ .../src/network/reqresp}/score.ts | 4 +- .../beacon-node/src/network/reqresp/types.ts | 40 +++ .../utils/collectSequentialBlocksInRange.ts | 53 +++ packages/reqresp/package.json | 1 - packages/reqresp/src/ReqResp.ts | 298 ++++++++--------- packages/reqresp/src/ReqRespProtocol.ts | 177 ---------- packages/reqresp/src/constants.ts | 12 - .../reqresp/src/encoders/responseEncode.ts | 3 +- .../encodingStrategies/sszSnappy/decode.ts | 2 +- packages/reqresp/src/index.ts | 7 +- packages/reqresp/src/interface.ts | 44 --- .../src/messages/BeaconBlocksByRange.ts | 19 ++ .../src/messages/BeaconBlocksByRangeV2.ts | 23 ++ .../src/messages/BeaconBlocksByRoot.ts | 20 ++ ...locksByRoot.ts => BeaconBlocksByRootV2.ts} | 22 +- packages/reqresp/src/messages/Goodbye.ts | 16 + .../messages/{v1 => }/LightClientBootstrap.ts | 17 +- .../{v1 => }/LightClientFinalityUpdate.ts | 17 +- .../{v1 => }/LightClientOptimisticUpdate.ts | 16 +- .../{v1 => }/LightClientUpdatesByRange.ts | 16 +- packages/reqresp/src/messages/Metadata.ts | 15 + packages/reqresp/src/messages/MetadataV2.ts | 15 + packages/reqresp/src/messages/Ping.ts | 16 + packages/reqresp/src/messages/Status.ts | 15 + packages/reqresp/src/messages/index.ts | 46 +-- packages/reqresp/src/messages/utils.ts | 3 - .../src/messages/v1/BeaconBlocksByRange.ts | 33 -- .../src/messages/v1/BeaconBlocksByRoot.ts | 34 -- packages/reqresp/src/messages/v1/Goodbye.ts | 28 -- packages/reqresp/src/messages/v1/Metadata.ts | 27 -- packages/reqresp/src/messages/v1/Ping.ts | 28 -- packages/reqresp/src/messages/v1/Status.ts | 25 -- .../src/messages/v2/BeaconBlocksByRange.ts | 37 --- packages/reqresp/src/messages/v2/Metadata.ts | 27 -- packages/reqresp/src/metrics.ts | 20 +- .../reqresp/src/rate_limiter/RateLimiter.ts | 11 +- .../reqresp/src/request/collectResponses.ts | 34 -- packages/reqresp/src/request/errors.ts | 4 +- packages/reqresp/src/request/index.ts | 57 ++-- packages/reqresp/src/response/index.ts | 21 +- packages/reqresp/src/sharedTypes.ts | 81 +---- packages/reqresp/src/types.ts | 86 +---- .../utils/assertSequentialBlocksInRange.ts | 57 ---- packages/reqresp/src/utils/collectExactOne.ts | 15 + .../reqresp/src/utils/collectMaxResponse.ts | 19 ++ packages/reqresp/src/utils/index.ts | 7 +- packages/reqresp/src/utils/multifork.ts | 9 - packages/reqresp/src/utils/protocolId.ts | 10 - 53 files changed, 872 insertions(+), 1135 deletions(-) delete mode 100644 packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts create mode 100644 packages/beacon-node/src/network/reqresp/interface.ts rename packages/{reqresp/src => beacon-node/src/network/reqresp}/score.ts (94%) create mode 100644 packages/beacon-node/src/network/reqresp/types.ts create mode 100644 packages/beacon-node/src/network/reqresp/utils/collectSequentialBlocksInRange.ts delete mode 100644 packages/reqresp/src/ReqRespProtocol.ts delete mode 100644 packages/reqresp/src/constants.ts create mode 100644 packages/reqresp/src/messages/BeaconBlocksByRange.ts create mode 100644 packages/reqresp/src/messages/BeaconBlocksByRangeV2.ts create mode 100644 packages/reqresp/src/messages/BeaconBlocksByRoot.ts rename packages/reqresp/src/messages/{v2/BeaconBlocksByRoot.ts => BeaconBlocksByRootV2.ts} (50%) create mode 100644 packages/reqresp/src/messages/Goodbye.ts rename packages/reqresp/src/messages/{v1 => }/LightClientBootstrap.ts (55%) rename packages/reqresp/src/messages/{v1 => }/LightClientFinalityUpdate.ts (51%) rename packages/reqresp/src/messages/{v1 => }/LightClientOptimisticUpdate.ts (52%) rename packages/reqresp/src/messages/{v1 => }/LightClientUpdatesByRange.ts (56%) create mode 100644 packages/reqresp/src/messages/Metadata.ts create mode 100644 packages/reqresp/src/messages/MetadataV2.ts create mode 100644 packages/reqresp/src/messages/Ping.ts create mode 100644 packages/reqresp/src/messages/Status.ts delete mode 100644 packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts delete mode 100644 packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts delete mode 100644 packages/reqresp/src/messages/v1/Goodbye.ts delete mode 100644 packages/reqresp/src/messages/v1/Metadata.ts delete mode 100644 packages/reqresp/src/messages/v1/Ping.ts delete mode 100644 packages/reqresp/src/messages/v1/Status.ts delete mode 100644 packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts delete mode 100644 packages/reqresp/src/messages/v2/Metadata.ts delete mode 100644 packages/reqresp/src/request/collectResponses.ts delete mode 100644 packages/reqresp/src/utils/assertSequentialBlocksInRange.ts create mode 100644 packages/reqresp/src/utils/collectExactOne.ts create mode 100644 packages/reqresp/src/utils/collectMaxResponse.ts delete mode 100644 packages/reqresp/src/utils/multifork.ts delete mode 100644 packages/reqresp/src/utils/protocolId.ts diff --git a/packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts b/packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts deleted file mode 100644 index 11d457f75563..000000000000 --- a/packages/beacon-node/src/network/reqresp/BeaconNodeReqResp.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {PeerId} from "@libp2p/interface-peer-id"; -import {ReqResp, ReqRespModules, ReqRespOptions, RequestTypedContainer} from "@lodestar/reqresp"; -import {RateLimiterOptions} from "@lodestar/reqresp/rate_limiter"; -import {INetworkEventBus, NetworkEvent} from "../events.js"; - -export interface BeaconNodeReqRespModules extends ReqRespModules { - networkEventBus: INetworkEventBus; -} - -export class BeaconNodeReqResp extends ReqResp { - private networkEventBus: INetworkEventBus; - - constructor(modules: BeaconNodeReqRespModules, options?: Partial & Partial) { - super(modules, options); - this.networkEventBus = modules.networkEventBus; - } - - protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { - // Allow onRequest to return and close the stream - // For Goodbye there may be a race condition where the listener of `receivedGoodbye` - // disconnects in the same syncronous call, preventing the stream from ending cleanly - setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); - } -} diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts index 53cd6fbbf5e6..788130bc07e3 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -1,8 +1,8 @@ import {EncodedPayload, EncodedPayloadType, ContextBytesType} from "@lodestar/reqresp"; -import {getSlotFromBytes} from "@lodestar/reqresp/utils"; import {allForks, phase0, Slot} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; +import {getSlotFromBytes} from "../../../util/multifork.js"; export async function* onBeaconBlocksByRoot( requestBody: phase0.BeaconBlocksByRootRequest, diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 33f9d99a8998..dea2c3046495 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -22,13 +22,13 @@ export function getReqRespHandlers({ db: IBeaconDb; chain: IBeaconChain; }): { - onStatus: HandlerTypeFromMessage; - onBeaconBlocksByRange: HandlerTypeFromMessage; - onBeaconBlocksByRoot: HandlerTypeFromMessage; - onLightClientBootstrap: HandlerTypeFromMessage; - onLightClientUpdatesByRange: HandlerTypeFromMessage; - onLightClientFinalityUpdate: HandlerTypeFromMessage; - onLightClientOptimisticUpdate: HandlerTypeFromMessage; + onStatus: HandlerTypeFromMessage; + onBeaconBlocksByRange: HandlerTypeFromMessage; + onBeaconBlocksByRoot: HandlerTypeFromMessage; + onLightClientBootstrap: HandlerTypeFromMessage; + onLightClientUpdatesByRange: HandlerTypeFromMessage; + onLightClientFinalityUpdate: HandlerTypeFromMessage; + onLightClientOptimisticUpdate: HandlerTypeFromMessage; } { return { async *onStatus() { diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index ceb9bb33d92d..ef1b8ff57f17 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -1,50 +1,269 @@ -import {getMetrics as getReqResMetrics, IReqResp, MetricsRegister} from "@lodestar/reqresp"; +import {Libp2p} from "libp2p"; +import {PeerId} from "@libp2p/interface-peer-id"; +import {ForkName} from "@lodestar/params"; +import {ILogger} from "@lodestar/utils"; +import {IBeaconConfig} from "@lodestar/config"; +import {ReqRespOpts} from "@lodestar/reqresp/lib/ReqResp.js"; + +import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; +import { + collectExactOne, + collectMaxResponse, + EncodedPayload, + EncodedPayloadType, + ReqResp, + RequestError, + ResponseError, +} from "@lodestar/reqresp"; import messages from "@lodestar/reqresp/messages"; -import {IMetrics} from "../../metrics/index.js"; -import {BeaconNodeReqResp, BeaconNodeReqRespModules} from "./BeaconNodeReqResp.js"; +import {IMetrics} from "../../metrics/metrics.js"; +import {INetworkEventBus, NetworkEvent} from "../events.js"; +import {IPeerRpcScoreStore} from "../peers/score.js"; +import {MetadataController} from "../metadata.js"; +import {PeerData, PeersData} from "../peers/peersData.js"; import {ReqRespHandlers} from "./handlers/index.js"; +import {collectSequentialBlocksInRange} from "./utils/collectSequentialBlocksInRange.js"; +import {IReqResp, RateLimiter, RespStatus} from "./interface.js"; +import {Method, RequestTypedContainer, Version} from "./types.js"; +import {onOutgoingReqRespError} from "./score.js"; -export const getBeaconNodeReqResp = ( - modules: Omit & { - metrics: IMetrics | null; - }, - reqRespHandlers: ReqRespHandlers -): IReqResp => { - const metrics = modules.metrics - ? getReqResMetrics((modules.metrics as unknown) as MetricsRegister, { - version: "", - commit: "", - network: "", - }) - : null; - - const reqRespModules = { - ...modules, - metrics, - } as BeaconNodeReqRespModules; - - const reqresp = new BeaconNodeReqResp(reqRespModules); - - reqresp.registerProtocol(messages.v1.Ping(reqRespModules)); - reqresp.registerProtocol(messages.v1.Status(reqRespModules, reqRespHandlers.onStatus)); - reqresp.registerProtocol(messages.v1.Metadata(reqRespModules)); - reqresp.registerProtocol(messages.v1.Goodbye(reqRespModules)); - reqresp.registerProtocol(messages.v1.BeaconBlocksByRange(reqRespModules, reqRespHandlers.onBeaconBlocksByRange)); - reqresp.registerProtocol(messages.v1.BeaconBlocksByRoot(reqRespModules, reqRespHandlers.onBeaconBlocksByRoot)); - reqresp.registerProtocol(messages.v1.LightClientBootstrap(reqRespModules, reqRespHandlers.onLightClientBootstrap)); - reqresp.registerProtocol( - messages.v1.LightClientFinalityUpdate(reqRespModules, reqRespHandlers.onLightClientFinalityUpdate) - ); - reqresp.registerProtocol( - messages.v1.LightClientOptimisticUpdate(reqRespModules, reqRespHandlers.onLightClientOptimisticUpdate) - ); - reqresp.registerProtocol( - messages.v1.LightClientUpdatesByRange(reqRespModules, reqRespHandlers.onLightClientUpdatesByRange) - ); - - reqresp.registerProtocol(messages.v2.Metadata(reqRespModules)); - reqresp.registerProtocol(messages.v2.BeaconBlocksByRange(reqRespModules, reqRespHandlers.onBeaconBlocksByRange)); - reqresp.registerProtocol(messages.v2.BeaconBlocksByRoot(reqRespModules, reqRespHandlers.onBeaconBlocksByRoot)); - - return reqresp; +/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ +export type ReqRespBlockResponse = { + /** Deserialized data of allForks.SignedBeaconBlock */ + bytes: Uint8Array; + slot: Slot; }; + +export interface ReqRespBeaconNodeModules { + libp2p: Libp2p; + peersData: PeersData; + logger: ILogger; + config: IBeaconConfig; + metrics: IMetrics | null; + reqRespHandlers: ReqRespHandlers; + metadataController: MetadataController; + peerRpcScores: IPeerRpcScoreStore; + networkEventBus: INetworkEventBus; +} + +/** + * Implementation of Ethereum Consensus p2p Req/Resp domain. + * For the spec that this code is based on, see: + * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain + * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain + */ +export class ReqRespBeaconNode extends ReqResp implements IReqResp { + private readonly reqRespHandlers: ReqRespHandlers; + private readonly metadataController: MetadataController; + private readonly peerRpcScores: IPeerRpcScoreStore; + private readonly inboundRateLimiter: RateLimiter; + private readonly networkEventBus: INetworkEventBus; + private readonly peerData: PeerData; + + constructor(modules: ReqRespBeaconNodeModules, options?: Partial & Partial) { + super(modules, {...defaultReqRespOptions, ...options}); + const reqRespHandlers: ReqRespHandlers = 1; + this.peerRpcScores = modules.peerRpcScores; + this.metadataController = modules.metadataController; + this.inboundRateLimiter = new InboundRateLimiter({...InboundRateLimiter.defaults, ...options}, modules); + + // TODO: Do not register everything! Some protocols are fork dependant + this.registerProtocol(messages.Ping(modules, this.onPing.bind(this))); + this.registerProtocol(messages.Status(modules, this.onStatus.bind(this))); + this.registerProtocol(messages.Metadata(modules, this.onMetadata.bind(this))); + this.registerProtocol(messages.MetadataV2(modules, this.onMetadata.bind(this))); + this.registerProtocol(messages.Goodbye(modules, this.onGoodbye.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRange(modules, this.onBeaconBlocksByRange.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRangeV2(modules, this.onBeaconBlocksByRange.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRoot(modules, this.onBeaconBlocksByRoot.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRootV2(modules, this.onBeaconBlocksByRoot.bind(this))); + this.registerProtocol(messages.LightClientBootstrap(modules, reqRespHandlers.onLightClientBootstrap)); + this.registerProtocol(messages.LightClientFinalityUpdate(modules, reqRespHandlers.onLightClientFinalityUpdate)); + this.registerProtocol(messages.LightClientOptimisticUpdate(modules, reqRespHandlers.onLightClientOptimisticUpdate)); + this.registerProtocol(messages.LightClientUpdatesByRange(modules, reqRespHandlers.onLightClientUpdatesByRange)); + } + + async start(): Promise { + await super.start(); + this.inboundRateLimiter.start(); + } + + async stop(): Promise { + await super.stop(); + this.inboundRateLimiter.stop(); + } + + pruneOnPeerDisconnect(peerId: PeerId): void { + this.inboundRateLimiter.prune(peerId); + } + + async status(peerId: PeerId, request: phase0.Status): Promise { + return collectExactOne( + this.sendRequest(peerId, Method.Status, [Version.V1], request) + ); + } + + async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { + // TODO: Replace with "ignore response after request" + await collectExactOne( + this.sendRequest(peerId, Method.Goodbye, [Version.V1], request) + ); + } + + async ping(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest(peerId, Method.Ping, [Version.V1], this.metadataController.seqNumber) + ); + } + + async metadata(peerId: PeerId, fork?: ForkName): Promise { + // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version + const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; + return collectExactOne(this.sendRequest(peerId, Method.Metadata, versions, null)); + } + + async beaconBlocksByRange( + peerId: PeerId, + request: phase0.BeaconBlocksByRangeRequest + ): Promise { + return collectSequentialBlocksInRange( + this.sendRequest( + peerId, + Method.BeaconBlocksByRange, + [Version.V2, Version.V1], // Prioritize V2 + request + ), + request + ); + } + + async beaconBlocksByRoot( + peerId: PeerId, + request: phase0.BeaconBlocksByRootRequest + ): Promise { + return collectMaxResponse( + this.sendRequest( + peerId, + Method.BeaconBlocksByRoot, + [Version.V2, Version.V1], // Prioritize V2 + request + ), + request.length + ); + } + + async lightClientBootstrap(peerId: PeerId, request: Root): Promise { + return collectExactOne( + this.sendRequest(peerId, Method.LightClientBootstrap, [Version.V1], request) + ); + } + + async lightClientOptimisticUpdate(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest( + peerId, + Method.LightClientOptimisticUpdate, + [Version.V1], + null + ) + ); + } + + async lightClientFinalityUpdate(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest( + peerId, + Method.LightClientFinalityUpdate, + [Version.V1], + null + ) + ); + } + + async lightClientUpdatesByRange( + peerId: PeerId, + request: altair.LightClientUpdatesByRange + ): Promise { + return collectMaxResponse( + this.sendRequest( + peerId, + Method.LightClientUpdatesByRange, + [Version.V1], + request + ), + request.count + ); + } + + protected sendRequest(peerId: PeerId, method: string, versions: number[], body: Req): AsyncIterable { + // Remember prefered encoding + const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; + + return super.sendRequest(peerId, method, versions, encoding, body); + } + + protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { + // Allow onRequest to return and close the stream + // For Goodbye there may be a race condition where the listener of `receivedGoodbye` + // disconnects in the same syncronous call, preventing the stream from ending cleanly + setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); + } + + protected onIncomingRequest(peerId: PeerId, method: Method): void { + if (method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + } + + protected onOutgoingRequestError(peerId: PeerId, method: Method, error: RequestError): void { + const peerAction = onOutgoingReqRespError(error, method); + if (peerAction !== null) { + this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); + } + } + + private async *onStatus(req: phase0.Status, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Status, body: req}, peerId); + // Remember prefered encoding + const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; + yield* this.reqRespHandlers.onStatus(req, peerId); + } + + private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); + yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; + } + + private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); + yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; + } + + private async *onMetadata(req: null, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); + + // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property + // It's safe to return altair.Metadata here for all versions + yield {type: EncodedPayloadType.ssz, data: this.metadataController.json}; + } + + private async *onBeaconBlocksByRange( + req: phase0.BeaconBlocksByRangeRequest, + peerId: PeerId + ): AsyncIterable> { + if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + yield* this.reqRespHandlers.onBeaconBlocksByRange(req, peerId); + } + + private async *onBeaconBlocksByRoot( + req: phase0.BeaconBlocksByRootRequest, + peerId: PeerId + ): AsyncIterable> { + if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + yield* this.reqRespHandlers.onBeaconBlocksByRoot(req, peerId); + } +} diff --git a/packages/beacon-node/src/network/reqresp/interface.ts b/packages/beacon-node/src/network/reqresp/interface.ts new file mode 100644 index 000000000000..641b7d4504fc --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/interface.ts @@ -0,0 +1,69 @@ +import {PeerId} from "@libp2p/interface-peer-id"; +import {ForkName} from "@lodestar/params"; +import {allForks, altair, phase0} from "@lodestar/types"; + +export interface IReqResp { + start(): void; + stop(): void; + status(peerId: PeerId, request: phase0.Status): Promise; + goodbye(peerId: PeerId, request: phase0.Goodbye): Promise; + ping(peerId: PeerId): Promise; + metadata(peerId: PeerId, fork?: ForkName): Promise; + beaconBlocksByRange( + peerId: PeerId, + request: phase0.BeaconBlocksByRangeRequest + ): Promise; + beaconBlocksByRoot(peerId: PeerId, request: phase0.BeaconBlocksByRootRequest): Promise; + pruneOnPeerDisconnect(peerId: PeerId): void; + lightClientBootstrap(peerId: PeerId, request: Uint8Array): Promise; + lightClientOptimisticUpdate(peerId: PeerId): Promise; + lightClientFinalityUpdate(peerId: PeerId): Promise; + lightClientUpdatesByRange( + peerId: PeerId, + request: altair.LightClientUpdatesByRange + ): Promise; +} + +/** + * Rate limiter interface for inbound and outbound requests. + */ +export interface RateLimiter { + /** Allow to request or response based on rate limit params configured. */ + allowRequest(peerId: PeerId): boolean; + /** Rate limit check for block count */ + allowBlockByRequest(peerId: PeerId, numBlock: number): boolean; + + /** + * Prune by peer id + */ + prune(peerId: PeerId): void; + start(): void; + stop(): void; +} + +// Request/Response constants +export enum RespStatus { + /** + * A normal response follows, with contents matching the expected message schema and encoding specified in the request + */ + SUCCESS = 0, + /** + * The contents of the request are semantically invalid, or the payload is malformed, + * or could not be understood. The response payload adheres to the ErrorMessage schema + */ + INVALID_REQUEST = 1, + /** + * The responder encountered an error while processing the request. The response payload adheres to the ErrorMessage schema + */ + SERVER_ERROR = 2, + /** + * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema (described below). Note: This response code is only valid as a response to BlocksByRange + */ + RESOURCE_UNAVAILABLE = 3, + /** + * Our node does not have bandwidth to serve requests due to either per-peer quota or total quota. + */ + RATE_LIMITED = 139, +} + +export type RpcResponseStatusError = Exclude; diff --git a/packages/reqresp/src/score.ts b/packages/beacon-node/src/network/reqresp/score.ts similarity index 94% rename from packages/reqresp/src/score.ts rename to packages/beacon-node/src/network/reqresp/score.ts index 9a121f9430fa..157e5d979ec6 100644 --- a/packages/reqresp/src/score.ts +++ b/packages/beacon-node/src/network/reqresp/score.ts @@ -1,6 +1,6 @@ +import {RequestError, RequestErrorCode} from "@lodestar/reqresp"; +import {PeerAction} from "../peers/score.js"; import {Method} from "./types.js"; -import {RequestError, RequestErrorCode} from "./request/index.js"; -import {PeerAction} from "./sharedTypes.js"; /** * libp2p-ts does not include types for the error codes. diff --git a/packages/beacon-node/src/network/reqresp/types.ts b/packages/beacon-node/src/network/reqresp/types.ts new file mode 100644 index 000000000000..c23f5c3b9717 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/types.ts @@ -0,0 +1,40 @@ +import {phase0} from "@lodestar/types"; + +/** ReqResp protocol names or methods. Each Method can have multiple versions and encodings */ +export enum Method { + // Phase 0 + Status = "status", + Goodbye = "goodbye", + Ping = "ping", + Metadata = "metadata", + BeaconBlocksByRange = "beacon_blocks_by_range", + BeaconBlocksByRoot = "beacon_blocks_by_root", + LightClientBootstrap = "light_client_bootstrap", + LightClientUpdatesByRange = "light_client_updates_by_range", + LightClientFinalityUpdate = "light_client_finality_update", + LightClientOptimisticUpdate = "light_client_optimistic_update", +} + +// To typesafe events to network +type RequestBodyByMethod = { + [Method.Status]: phase0.Status; + [Method.Goodbye]: phase0.Goodbye; + [Method.Ping]: phase0.Ping; + [Method.Metadata]: null; + // Do not matter + [Method.BeaconBlocksByRange]: unknown; + [Method.BeaconBlocksByRoot]: unknown; + [Method.LightClientBootstrap]: unknown; + [Method.LightClientUpdatesByRange]: unknown; + [Method.LightClientFinalityUpdate]: unknown; + [Method.LightClientOptimisticUpdate]: unknown; +}; + +export type RequestTypedContainer = { + [K in Method]: {method: K; body: RequestBodyByMethod[K]}; +}[Method]; + +export enum Version { + V1 = 1, + V2 = 2, +} diff --git a/packages/beacon-node/src/network/reqresp/utils/collectSequentialBlocksInRange.ts b/packages/beacon-node/src/network/reqresp/utils/collectSequentialBlocksInRange.ts new file mode 100644 index 000000000000..b162677b32d0 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/utils/collectSequentialBlocksInRange.ts @@ -0,0 +1,53 @@ +import {allForks, phase0} from "@lodestar/types"; +import {LodestarError} from "@lodestar/utils"; + +/** + * Asserts a response from BeaconBlocksByRange respects the request and is sequential + * Note: MUST allow missing block for skipped slots. + */ +export async function collectSequentialBlocksInRange( + blockStream: AsyncIterable, + {count, startSlot}: phase0.BeaconBlocksByRangeRequest +): Promise { + const blocks: allForks.SignedBeaconBlock[] = []; + + for await (const block of blockStream) { + const blockSlot = block.message.slot; + + // Note: step is deprecated and assumed to be 1 + if (blockSlot >= startSlot + count) { + throw new BlocksByRangeError({code: BlocksByRangeErrorCode.OVER_MAX_SLOT}); + } + + if (blockSlot < startSlot) { + throw new BlocksByRangeError({code: BlocksByRangeErrorCode.UNDER_START_SLOT}); + } + + const prevBlock = blocks.length === 0 ? null : blocks[blocks.length - 1]; + if (prevBlock) { + if (prevBlock.message.slot >= blockSlot) { + throw new BlocksByRangeError({code: BlocksByRangeErrorCode.BAD_SEQUENCE}); + } + } + + blocks.push(block); + if (blocks.length >= count) { + break; // Done, collected all blocks + } + } + + return blocks; +} + +export enum BlocksByRangeErrorCode { + UNDER_START_SLOT = "BLOCKS_BY_RANGE_ERROR_UNDER_START_SLOT", + OVER_MAX_SLOT = "BLOCKS_BY_RANGE_ERROR_OVER_MAX_SLOT", + BAD_SEQUENCE = "BLOCKS_BY_RANGE_ERROR_BAD_SEQUENCE", +} + +type BlocksByRangeErrorType = + | {code: BlocksByRangeErrorCode.UNDER_START_SLOT} + | {code: BlocksByRangeErrorCode.OVER_MAX_SLOT} + | {code: BlocksByRangeErrorCode.BAD_SEQUENCE}; + +export class BlocksByRangeError extends LodestarError {} diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index e67b2a0b0d08..69279321b5e1 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -59,7 +59,6 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@libp2p/interface-peer-id": "^1.0.4", "@libp2p/interface-connection": "^3.0.2", "@libp2p/interface-peer-id": "^1.0.4", "strict-event-emitter-types": "^2.0.0", diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index 7d0c4973ffbd..233a9c467037 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -1,34 +1,30 @@ +import {setMaxListeners} from "node:events"; +import {Connection, Stream} from "@libp2p/interface-connection"; import {PeerId} from "@libp2p/interface-peer-id"; -import {ForkName} from "@lodestar/params"; -import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; -import {DIAL_TIMEOUT, REQUEST_TIMEOUT, RESP_TIMEOUT, TTFB_TIMEOUT} from "./constants.js"; -import {IReqResp, RateLimiter, ReqRespHandlerContext, ReqRespModules, RespStatus} from "./interface.js"; -import {InboundRateLimiter, RateLimiterOptions} from "./rate_limiter/RateLimiter.js"; -import {ReqRespProtocol} from "./ReqRespProtocol.js"; -import {RequestError} from "./request/errors.js"; -import {ResponseError} from "./response/errors.js"; -import {onOutgoingReqRespError} from "./score.js"; -import {IPeerRpcScoreStore, MetadataController} from "./sharedTypes.js"; -import {Method, ReqRespOptions, Version} from "./types.js"; -import {assertSequentialBlocksInRange} from "./utils/index.js"; - -/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ -export type ReqRespBlockResponse = { - /** Deserialized data of allForks.SignedBeaconBlock */ - bytes: Uint8Array; - slot: Slot; -}; - -export const defaultReqRespOptions: ReqRespOptions = { - // eslint-disable-next-line @typescript-eslint/naming-convention - TTFB_TIMEOUT, - // eslint-disable-next-line @typescript-eslint/naming-convention - RESP_TIMEOUT, - // eslint-disable-next-line @typescript-eslint/naming-convention - REQUEST_TIMEOUT, - // eslint-disable-next-line @typescript-eslint/naming-convention - DIAL_TIMEOUT, -}; +import {Libp2p} from "libp2p"; +import {ILogger} from "@lodestar/utils"; +import {IBeaconConfig} from "@lodestar/config"; +import {getMetrics, Metrics, MetricsRegister} from "./metrics.js"; +import {RequestError, RequestErrorCode, sendRequest, SendRequestOpts} from "./request/index.js"; +import {handleRequest} from "./response/index.js"; +import {Encoding, ProtocolDefinition} from "./types.js"; + +type ProtocolID = string; + +export const DEFAULT_PROTOCOL_PREFIX = "/eth2/beacon_chain/req"; + +export interface ReqRespProtocolModules { + libp2p: Libp2p; + logger: ILogger; + config: IBeaconConfig; + metrics: Metrics | null; +} + +export interface ReqRespOpts extends SendRequestOpts { + /** Custom prefix for `/ProtocolPrefix/MessageName/SchemaVersion/Encoding` */ + protocolPrefix?: string; + getPeerLogMetadata?: (peerId: string) => string; +} /** * Implementation of Ethereum Consensus p2p Req/Resp domain. @@ -36,145 +32,151 @@ export const defaultReqRespOptions: ReqRespOptions = { * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ -export abstract class ReqResp extends ReqRespProtocol implements IReqResp { - private inboundRateLimiter: RateLimiter; - private peerRpcScores: IPeerRpcScoreStore; - private metadataController: MetadataController; - - constructor(modules: ReqRespModules, options?: Partial & Partial) { - super(modules, {...defaultReqRespOptions, ...options}); - this.peerRpcScores = modules.peerRpcScores; - this.metadataController = modules.metadataController; - this.inboundRateLimiter = new InboundRateLimiter({...InboundRateLimiter.defaults, ...options}, modules); - } +export abstract class ReqResp { + private readonly libp2p: Libp2p; + private readonly logger: ILogger; + private readonly metrics: Metrics | null; + private controller = new AbortController(); + /** Tracks request and responses in a sequential counter */ + private reqCount = 0; + private readonly protocolPrefix: string; - protected onIncomingRequest(peerId: PeerId, method: Method): void { - if (method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - } + /** `${protocolPrefix}/${method}/${version}/${encoding}` */ + private readonly supportedProtocols = new Map(); - protected onOutgoingRequestError(peerId: PeerId, method: Method, error: RequestError): void { - const peerAction = onOutgoingReqRespError(error, method); - if (peerAction !== null) { - this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); - } + constructor(modules: ReqRespProtocolModules, private readonly opts: ReqRespOpts) { + this.libp2p = modules.libp2p; + this.logger = modules.logger; + this.metrics = modules.metrics ? getMetrics((modules.metrics as unknown) as MetricsRegister) : null; + this.protocolPrefix = opts.protocolPrefix ?? DEFAULT_PROTOCOL_PREFIX; } - protected getContext(): ReqRespHandlerContext { - const context = super.getContext(); - return { - ...context, - modules: { - ...context.modules, - inboundRateLimiter: this.inboundRateLimiter, - metadataController: this.metadataController, - }, - }; + registerProtocol(protocol: ProtocolDefinition): void { + const {method, version, encoding} = protocol; + const protocolID = this.formatProtocolID(method, version, encoding); + this.supportedProtocols.set(protocolID, protocol as ProtocolDefinition); } async start(): Promise { - await super.start(); - this.inboundRateLimiter.start(); - } - - async stop(): Promise { - await super.stop(); - this.inboundRateLimiter.stop(); - } - - pruneOnPeerDisconnect(peerId: PeerId): void { - this.inboundRateLimiter.prune(peerId); - } + this.controller = new AbortController(); + // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 + // Since it is perfectly fine to have listeners > 10 + setMaxListeners(Infinity, this.controller.signal); - async status(peerId: PeerId, request: phase0.Status): Promise { - return await this.sendRequest(peerId, Method.Status, [Version.V1], request); - } - - async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { - await this.sendRequest(peerId, Method.Goodbye, [Version.V1], request); - } - - async ping(peerId: PeerId): Promise { - return await this.sendRequest( - peerId, - Method.Ping, - [Version.V1], - this.metadataController.seqNumber - ); + for (const [protocolID, protocol] of this.supportedProtocols) { + await this.libp2p.handle(protocolID, this.getRequestHandler(protocol)); + } } - async metadata(peerId: PeerId, fork?: ForkName): Promise { - // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version - const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; - return await this.sendRequest(peerId, Method.Metadata, versions, null); + async stop(): Promise { + for (const protocolID of this.supportedProtocols.keys()) { + await this.libp2p.unhandle(protocolID); + } + this.controller.abort(); } - async beaconBlocksByRange( + // Helper to reduce code duplication + protected async *sendRequest( peerId: PeerId, - request: phase0.BeaconBlocksByRangeRequest - ): Promise { - const blocks = await this.sendRequest( - peerId, - Method.BeaconBlocksByRange, - [Version.V2, Version.V1], // Prioritize V2 - request, - request.count - ); - assertSequentialBlocksInRange(blocks, request); - return blocks; - } + method: string, + versions: number[], + encoding: Encoding, + body: Req + ): AsyncIterable { + const peerClient = this.opts.getPeerLogMetadata?.(peerId.toString()); + this.metrics?.outgoingRequests.inc({method}); + const timer = this.metrics?.outgoingRequestRoundtripTime.startTimer({method}); + + const protocols: ProtocolDefinition[] = []; + const protocolIDs: string[] = []; + + for (const version of versions) { + const protocolID = this.formatProtocolID(method, version, encoding); + const protocol = this.supportedProtocols.get(protocolID); + if (!protocol) { + throw Error(`Request to send to protocol ${protocolID} but it has not been declared`); + } + protocols.push(protocol); + protocolIDs.push(protocolID); + } - async beaconBlocksByRoot( - peerId: PeerId, - request: phase0.BeaconBlocksByRootRequest - ): Promise { - return await this.sendRequest( - peerId, - Method.BeaconBlocksByRoot, - [Version.V2, Version.V1], // Prioritize V2 - request, - request.length - ); + try { + yield* sendRequest( + {logger: this.logger, libp2p: this.libp2p, peerClient}, + peerId, + protocols, + protocolIDs, + body, + this.controller.signal, + this.opts, + this.reqCount++ + ); + } catch (e) { + this.metrics?.outgoingErrors.inc({method}); + + if (e instanceof RequestError) { + if (e.type.code === RequestErrorCode.DIAL_ERROR || e.type.code === RequestErrorCode.DIAL_TIMEOUT) { + this.metrics?.dialErrors.inc(); + } + + this.onOutgoingRequestError(peerId, method, e); + } + + throw e; + } finally { + timer?.(); + } } - async lightClientBootstrap(peerId: PeerId, request: Root): Promise { - return await this.sendRequest( - peerId, - Method.LightClientBootstrap, - [Version.V1], - request - ); + private getRequestHandler(protocol: ProtocolDefinition) { + return async ({connection, stream}: {connection: Connection; stream: Stream}) => { + const peerId = connection.remotePeer; + const peerClient = this.opts.getPeerLogMetadata?.(peerId.toString()); + const method = protocol.method; + + this.metrics?.incomingRequests.inc({method}); + const timer = this.metrics?.incomingRequestHandlerTime.startTimer({method}); + + this.onIncomingRequest?.(peerId, method); + + try { + await handleRequest({ + logger: this.logger, + stream, + peerId, + protocol, + signal: this.controller.signal, + requestId: this.reqCount++, + peerClient, + requestTimeoutMs: this.opts.requestTimeoutMs, + }); + // TODO: Do success peer scoring here + } catch { + this.metrics?.incomingErrors.inc({method}); + + // TODO: Do error peer scoring here + // Must not throw since this is an event handler + } finally { + timer?.(); + } + }; } - async lightClientOptimisticUpdate(peerId: PeerId): Promise { - return await this.sendRequest( - peerId, - Method.LightClientOptimisticUpdate, - [Version.V1], - null - ); + protected onIncomingRequest(_peerId: PeerId, _method: string): void { + // Override } - async lightClientFinalityUpdate(peerId: PeerId): Promise { - return await this.sendRequest( - peerId, - Method.LightClientFinalityUpdate, - [Version.V1], - null - ); + protected onOutgoingRequestError(_peerId: PeerId, _method: string, _error: RequestError): void { + // Override } - async lightClientUpdate( - peerId: PeerId, - request: altair.LightClientUpdatesByRange - ): Promise { - return await this.sendRequest( - peerId, - Method.LightClientUpdatesByRange, - [Version.V1], - request, - request.count - ); + /** + * ``` + * /ProtocolPrefix/MessageName/SchemaVersion/Encoding + * ``` + * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification + */ + protected formatProtocolID(method: string, version: number, encoding: Encoding): string { + return `${this.protocolPrefix}/${method}/${version}/${encoding}`; } } diff --git a/packages/reqresp/src/ReqRespProtocol.ts b/packages/reqresp/src/ReqRespProtocol.ts deleted file mode 100644 index c5a73592e5cd..000000000000 --- a/packages/reqresp/src/ReqRespProtocol.ts +++ /dev/null @@ -1,177 +0,0 @@ -import {setMaxListeners} from "node:events"; -import {Connection, Stream} from "@libp2p/interface-connection"; -import {PeerId} from "@libp2p/interface-peer-id"; -import {Libp2p} from "libp2p"; -import {ILogger} from "@lodestar/utils"; -import {IBeaconConfig} from "@lodestar/config"; -import {Metrics} from "./metrics.js"; -import {RequestError, RequestErrorCode, sendRequest} from "./request/index.js"; -import {handleRequest} from "./response/index.js"; -import {PeersData} from "./sharedTypes.js"; -import {Encoding, Method, ProtocolDefinition, ReqRespOptions, RequestTypedContainer} from "./types.js"; -import {formatProtocolID} from "./utils/index.js"; -import {ReqRespHandlerProtocolContext} from "./interface.js"; - -type ProtocolID = string; - -export interface ReqRespProtocolModules { - libp2p: Libp2p; - peersData: PeersData; - logger: ILogger; - config: IBeaconConfig; - metrics: Metrics | null; -} - -/** - * Implementation of Ethereum Consensus p2p Req/Resp domain. - * For the spec that this code is based on, see: - * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain - * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain - */ -export abstract class ReqRespProtocol { - private libp2p: Libp2p; - private readonly peersData: PeersData; - private logger: ILogger; - private controller = new AbortController(); - private options: ReqRespOptions; - private reqCount = 0; - private respCount = 0; - private metrics: Metrics | null; - private config: IBeaconConfig; - - /** `${protocolPrefix}/${method}/${version}/${encoding}` */ - private readonly supportedProtocols = new Map(); - - constructor(modules: ReqRespProtocolModules, options: ReqRespOptions) { - this.options = options; - this.config = modules.config; - this.libp2p = modules.libp2p; - this.peersData = modules.peersData; - this.logger = modules.logger; - this.metrics = modules.metrics; - } - - registerProtocol(protocol: ProtocolDefinition): void { - const {method, version, encoding} = protocol; - const protocolID = formatProtocolID(method, version, encoding); - this.supportedProtocols.set(protocolID, protocol as ProtocolDefinition); - } - - async start(): Promise { - this.controller = new AbortController(); - // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 - // Since it is perfectly fine to have listeners > 10 - setMaxListeners(Infinity, this.controller.signal); - - for (const [protocolID, protocol] of this.supportedProtocols) { - await this.libp2p.handle(protocolID, this.getRequestHandler(protocol)); - } - } - - async stop(): Promise { - for (const protocolID of this.supportedProtocols.keys()) { - await this.libp2p.unhandle(protocolID); - } - this.controller.abort(); - } - - // Helper to reduce code duplication - protected async sendRequest( - peerId: PeerId, - method: string, - versions: string[], - body: Req, - maxResponses = 1 - ): Promise { - const peerClient = this.peersData.getPeerKind(peerId.toString()); - this.metrics?.outgoingRequests.inc({method}); - const timer = this.metrics?.outgoingRequestRoundtripTime.startTimer({method}); - - // Remember prefered encoding - const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; - - const protocols: ProtocolDefinition[] = []; - for (const version of versions) { - const protocolID = formatProtocolID(method, version, encoding); - const protocol = this.supportedProtocols.get(protocolID); - if (!protocol) { - throw Error(`Request to send to protocol ${protocolID} but it has not been declared`); - } - protocols.push(protocol); - } - - try { - const result = await sendRequest( - {logger: this.logger, libp2p: this.libp2p, peerClient}, - peerId, - protocols, - body, - maxResponses, - this.controller.signal, - this.options, - this.reqCount++ - ); - - return result; - } catch (e) { - this.metrics?.outgoingErrors.inc({method}); - - if (e instanceof RequestError) { - if (e.type.code === RequestErrorCode.DIAL_ERROR || e.type.code === RequestErrorCode.DIAL_TIMEOUT) { - this.metrics?.dialErrors.inc(); - } - - this.onOutgoingRequestError(peerId, method as Method, e); - } - - throw e; - } finally { - timer?.(); - } - } - - private getRequestHandler(protocol: ProtocolDefinition) { - return async ({connection, stream}: {connection: Connection; stream: Stream}) => { - const peerId = connection.remotePeer; - const peerClient = this.peersData.getPeerKind(peerId.toString()); - const method = protocol.method; - - this.metrics?.incomingRequests.inc({method}); - const timer = this.metrics?.incomingRequestHandlerTime.startTimer({method}); - - this.onIncomingRequest?.(peerId, method); - - try { - await handleRequest({ - context: this.getContext(), - logger: this.logger, - stream, - peerId, - protocol, - signal: this.controller.signal, - requestId: this.respCount++, - peerClient, - }); - // TODO: Do success peer scoring here - } catch { - this.metrics?.incomingErrors.inc({method}); - - // TODO: Do error peer scoring here - // Must not throw since this is an event handler - } finally { - timer?.(); - } - }; - } - - protected getContext(): Context { - return { - modules: {config: this.config, logger: this.logger, metrics: this.metrics, peersData: this.peersData}, - eventHandlers: {onIncomingRequestBody: this.onIncomingRequestBody}, - } as Context; - } - - protected abstract onIncomingRequestBody(_req: RequestTypedContainer, _peerId: PeerId): void; - protected abstract onOutgoingRequestError(_peerId: PeerId, _method: Method, _error: RequestError): void; - protected abstract onIncomingRequest(_peerId: PeerId, _method: Method): void; -} diff --git a/packages/reqresp/src/constants.ts b/packages/reqresp/src/constants.ts deleted file mode 100644 index 603671c6d297..000000000000 --- a/packages/reqresp/src/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** The maximum time for complete response transfer. */ -export const RESP_TIMEOUT = 10 * 1000; // 10 sec -/** Non-spec timeout from sending request until write stream closed by responder */ -export const REQUEST_TIMEOUT = 5 * 1000; // 5 sec -/** The maximum time to wait for first byte of request response (time-to-first-byte). */ -export const TTFB_TIMEOUT = 5 * 1000; // 5 sec -/** Non-spec timeout from dialing protocol until stream opened */ -export const DIAL_TIMEOUT = 5 * 1000; // 5 sec -// eslint-disable-next-line @typescript-eslint/naming-convention -export const timeoutOptions = {TTFB_TIMEOUT, RESP_TIMEOUT, REQUEST_TIMEOUT, DIAL_TIMEOUT}; - -export const MAX_VARINT_BYTES = 10; diff --git a/packages/reqresp/src/encoders/responseEncode.ts b/packages/reqresp/src/encoders/responseEncode.ts index 8561db1c0705..448d9b2a91da 100644 --- a/packages/reqresp/src/encoders/responseEncode.ts +++ b/packages/reqresp/src/encoders/responseEncode.ts @@ -4,7 +4,6 @@ import {encodeErrorMessage} from "../utils/index.js"; import { ContextBytesType, ContextBytesFactory, - Protocol, ProtocolDefinition, EncodedPayload, EncodedPayloadType, @@ -53,7 +52,7 @@ export function responseEncodeSuccess( * fn yields exactly one `` and afterwards the stream must be terminated */ export async function* responseEncodeError( - protocol: Protocol, + protocol: Pick, status: RpcResponseStatusError, errorMessage: string ): AsyncGenerator { diff --git a/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts index 959de9c45371..4abe4e19c628 100644 --- a/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts +++ b/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts @@ -5,8 +5,8 @@ import {BufferedSource} from "../../utils/index.js"; import {SnappyFramesUncompress} from "./snappyFrames/uncompress.js"; import {maxEncodedLen} from "./utils.js"; import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; -import {MAX_VARINT_BYTES} from "../../constants.js"; +export const MAX_VARINT_BYTES = 10; export type TypeRead = Pick, "minSize" | "maxSize" | "deserialize">; /** diff --git a/packages/reqresp/src/index.ts b/packages/reqresp/src/index.ts index f9ecccadf746..6a4dadce7ce8 100644 --- a/packages/reqresp/src/index.ts +++ b/packages/reqresp/src/index.ts @@ -1,7 +1,8 @@ export {ReqResp} from "./ReqResp.js"; export {getMetrics, Metrics, MetricsRegister} from "./metrics.js"; -export {Encoding as ReqRespEncoding, Method as ReqRespMethod} from "./types.js"; // Expose enums renamed +export {Encoding as ReqRespEncoding} from "./types.js"; // Expose enums renamed export * from "./types.js"; export * from "./interface.js"; -export * from "./constants.js"; -export * from "./response/errors.js"; +export {ResponseErrorCode, ResponseError} from "./response/errors.js"; +export {RequestErrorCode, RequestError} from "./request/errors.js"; +export {collectExactOne, collectMaxResponse} from "./utils/index.js"; diff --git a/packages/reqresp/src/interface.ts b/packages/reqresp/src/interface.ts index df2a441c5dde..4aab7414f5c1 100644 --- a/packages/reqresp/src/interface.ts +++ b/packages/reqresp/src/interface.ts @@ -1,48 +1,4 @@ import {PeerId} from "@libp2p/interface-peer-id"; -import {ForkName} from "@lodestar/params"; -import {allForks, altair, phase0} from "@lodestar/types"; -import {ReqRespProtocolModules} from "./ReqRespProtocol.js"; -import {IPeerRpcScoreStore, MetadataController} from "./sharedTypes.js"; -import {ProtocolDefinition, RequestTypedContainer} from "./types.js"; - -export interface IReqResp { - start(): void; - stop(): void; - status(peerId: PeerId, request: phase0.Status): Promise; - goodbye(peerId: PeerId, request: phase0.Goodbye): Promise; - ping(peerId: PeerId): Promise; - metadata(peerId: PeerId, fork?: ForkName): Promise; - beaconBlocksByRange( - peerId: PeerId, - request: phase0.BeaconBlocksByRangeRequest - ): Promise; - beaconBlocksByRoot(peerId: PeerId, request: phase0.BeaconBlocksByRootRequest): Promise; - pruneOnPeerDisconnect(peerId: PeerId): void; - lightClientBootstrap(peerId: PeerId, request: Uint8Array): Promise; - lightClientOptimisticUpdate(peerId: PeerId): Promise; - lightClientFinalityUpdate(peerId: PeerId): Promise; - lightClientUpdate(peerId: PeerId, request: altair.LightClientUpdatesByRange): Promise; - registerProtocol(protocol: ProtocolDefinition): void; -} - -export interface ReqRespModules extends ReqRespProtocolModules { - peerRpcScores: IPeerRpcScoreStore; - metadataController: MetadataController; -} - -export interface ReqRespHandlerProtocolContext { - modules: Omit; - eventHandlers: { - onIncomingRequestBody(_req: RequestTypedContainer, _peerId: PeerId): void; - }; -} - -export interface ReqRespHandlerContext extends ReqRespHandlerProtocolContext { - modules: ReqRespHandlerProtocolContext["modules"] & { - inboundRateLimiter: RateLimiter; - metadataController: MetadataController; - }; -} /** * Rate limiter interface for inbound and outbound requests. diff --git a/packages/reqresp/src/messages/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/BeaconBlocksByRange.ts new file mode 100644 index 000000000000..12d0af7f9b5a --- /dev/null +++ b/packages/reqresp/src/messages/BeaconBlocksByRange.ts @@ -0,0 +1,19 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRange: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRangeRequest, + allForks.SignedBeaconBlock +> = (_modules, handler) => { + return { + method: "beacon_blocks_by_range", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/BeaconBlocksByRangeV2.ts b/packages/reqresp/src/messages/BeaconBlocksByRangeV2.ts new file mode 100644 index 000000000000..756f484fd521 --- /dev/null +++ b/packages/reqresp/src/messages/BeaconBlocksByRangeV2.ts @@ -0,0 +1,23 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRangeV2: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRangeRequest, + allForks.SignedBeaconBlock +> = (modules, handler) => { + return { + method: "beacon_blocks_by_range", + version: 2, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, + contextBytes: { + type: ContextBytesType.ForkDigest, + forkDigestContext: modules.config, + forkFromResponse: (block) => modules.config.getForkName(block.message.slot), + }, + }; +}; diff --git a/packages/reqresp/src/messages/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/BeaconBlocksByRoot.ts new file mode 100644 index 000000000000..b4e22d471b45 --- /dev/null +++ b/packages/reqresp/src/messages/BeaconBlocksByRoot.ts @@ -0,0 +1,20 @@ +import {allForks, phase0, ssz} from "@lodestar/types"; +import {toHex} from "@lodestar/utils"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const BeaconBlocksByRoot: ProtocolDefinitionGenerator< + phase0.BeaconBlocksByRootRequest, + allForks.SignedBeaconBlock +> = (_modules, handler) => { + return { + method: "beacon_blocks_by_root", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => ssz.phase0.BeaconBlocksByRootRequest, + responseType: (forkName) => ssz[forkName].SignedBeaconBlock, + renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/BeaconBlocksByRootV2.ts similarity index 50% rename from packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts rename to packages/reqresp/src/messages/BeaconBlocksByRootV2.ts index aacef8789177..76aadd8b0580 100644 --- a/packages/reqresp/src/messages/v2/BeaconBlocksByRoot.ts +++ b/packages/reqresp/src/messages/BeaconBlocksByRootV2.ts @@ -1,30 +1,17 @@ import {allForks, phase0, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; -import {RespStatus} from "../../interface.js"; -import {ResponseError} from "../../response/errors.js"; -import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getHandlerRequiredErrorFor} from "../utils.js"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const BeaconBlocksByRootV2: ProtocolDefinitionGenerator< phase0.BeaconBlocksByRootRequest, allForks.SignedBeaconBlock > = (modules, handler) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRoot); - } - return { - method: Method.BeaconBlocksByRoot, - version: Version.V2, + method: "beacon_blocks_by_root", + version: 2, encoding: Encoding.SSZ_SNAPPY, - handler: async function* beaconBlocksByRootV2Handler(context, req, peerId) { - if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - - yield* handler(req, peerId); - }, + handler, requestType: () => ssz.phase0.BeaconBlocksByRootRequest, responseType: (forkName) => ssz[forkName].SignedBeaconBlock, renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), @@ -33,6 +20,5 @@ export const BeaconBlocksByRootV2: ProtocolDefinitionGenerator< forkDigestContext: modules.config, forkFromResponse: (block) => modules.config.getForkName(block.message.slot), }, - isSingleResponse: false, }; }; diff --git a/packages/reqresp/src/messages/Goodbye.ts b/packages/reqresp/src/messages/Goodbye.ts new file mode 100644 index 000000000000..db9684466bc9 --- /dev/null +++ b/packages/reqresp/src/messages/Goodbye.ts @@ -0,0 +1,16 @@ +import {phase0, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Goodbye: ProtocolDefinitionGenerator = (_modules, handler) => { + return { + method: "goodbye", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => ssz.phase0.Goodbye, + responseType: () => ssz.phase0.Goodbye, + renderRequestBody: (req) => req.toString(10), + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/v1/LightClientBootstrap.ts b/packages/reqresp/src/messages/LightClientBootstrap.ts similarity index 55% rename from packages/reqresp/src/messages/v1/LightClientBootstrap.ts rename to packages/reqresp/src/messages/LightClientBootstrap.ts index e7448e913da4..7aa9e380c9b5 100644 --- a/packages/reqresp/src/messages/v1/LightClientBootstrap.ts +++ b/packages/reqresp/src/messages/LightClientBootstrap.ts @@ -1,28 +1,21 @@ import {altair, Root, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; -import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; +import {Encoding, ProtocolDefinitionGenerator} from "../types.js"; +import {getContextBytesLightclient} from "./utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientBootstrap: ProtocolDefinitionGenerator = ( modules, handler ) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.LightClientBootstrap); - } - return { - method: Method.LightClientBootstrap, - version: Version.V1, + method: "light_client_bootstrap", + version: 1, encoding: Encoding.SSZ_SNAPPY, - handler: async function* lightClientBootstrapHandler(context, req, peerId) { - yield* handler(req, peerId); - }, + handler, requestType: () => ssz.Root, responseType: () => ssz.altair.LightClientBootstrap, renderRequestBody: (req) => toHex(req), contextBytes: getContextBytesLightclient((bootstrap) => modules.config.getForkName(bootstrap.header.slot), modules), - isSingleResponse: true, }; }; diff --git a/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts b/packages/reqresp/src/messages/LightClientFinalityUpdate.ts similarity index 51% rename from packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts rename to packages/reqresp/src/messages/LightClientFinalityUpdate.ts index ad033e8df346..5f10c60391af 100644 --- a/packages/reqresp/src/messages/v1/LightClientFinalityUpdate.ts +++ b/packages/reqresp/src/messages/LightClientFinalityUpdate.ts @@ -1,26 +1,19 @@ import {altair, ssz} from "@lodestar/types"; -import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; +import {Encoding, ProtocolDefinitionGenerator} from "../types.js"; +import {getContextBytesLightclient} from "./utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientFinalityUpdate: ProtocolDefinitionGenerator = ( modules, handler ) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.LightClientFinalityUpdate); - } - return { - method: Method.LightClientFinalityUpdate, - version: Version.V1, + method: "light_client_finality_update", + version: 1, encoding: Encoding.SSZ_SNAPPY, - handler: async function* statusHandler(_context, req, peerId) { - yield* handler(req, peerId); - }, + handler, requestType: () => null, responseType: () => ssz.altair.LightClientFinalityUpdate, contextBytes: getContextBytesLightclient((update) => modules.config.getForkName(update.signatureSlot), modules), - isSingleResponse: true, }; }; diff --git a/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts b/packages/reqresp/src/messages/LightClientOptimisticUpdate.ts similarity index 52% rename from packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts rename to packages/reqresp/src/messages/LightClientOptimisticUpdate.ts index 614d12be87fc..d2f6d0849e20 100644 --- a/packages/reqresp/src/messages/v1/LightClientOptimisticUpdate.ts +++ b/packages/reqresp/src/messages/LightClientOptimisticUpdate.ts @@ -1,25 +1,19 @@ import {altair, ssz} from "@lodestar/types"; -import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; +import {Encoding, ProtocolDefinitionGenerator} from "../types.js"; +import {getContextBytesLightclient} from "./utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientOptimisticUpdate: ProtocolDefinitionGenerator = ( modules, handler ) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.LightClientOptimisticUpdate); - } return { - method: Method.LightClientOptimisticUpdate, - version: Version.V1, + method: "light_client_finality_update", + version: 1, encoding: Encoding.SSZ_SNAPPY, - handler: async function* statusHandler(_context, req, peerId) { - yield* handler(req, peerId); - }, + handler, requestType: () => null, responseType: () => ssz.altair.LightClientOptimisticUpdate, contextBytes: getContextBytesLightclient((update) => modules.config.getForkName(update.signatureSlot), modules), - isSingleResponse: true, }; }; diff --git a/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts b/packages/reqresp/src/messages/LightClientUpdatesByRange.ts similarity index 56% rename from packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts rename to packages/reqresp/src/messages/LightClientUpdatesByRange.ts index 74e05bdb4eb6..347aa2216815 100644 --- a/packages/reqresp/src/messages/v1/LightClientUpdatesByRange.ts +++ b/packages/reqresp/src/messages/LightClientUpdatesByRange.ts @@ -1,26 +1,20 @@ import {altair, ssz} from "@lodestar/types"; -import {Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getContextBytesLightclient, getHandlerRequiredErrorFor} from "../utils.js"; +import {Encoding, ProtocolDefinitionGenerator} from "../types.js"; +import {getContextBytesLightclient} from "./utils.js"; // eslint-disable-next-line @typescript-eslint/naming-convention export const LightClientUpdatesByRange: ProtocolDefinitionGenerator< altair.LightClientUpdatesByRange, altair.LightClientUpdate > = (modules, handler) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.LightClientUpdatesByRange); - } return { - method: Method.LightClientUpdatesByRange, - version: Version.V1, + method: "light_client_updates_by_range", + version: 1, encoding: Encoding.SSZ_SNAPPY, - handler: async function* statusHandler(_context, req, peerId) { - yield* handler(req, peerId); - }, + handler, requestType: () => ssz.altair.LightClientUpdatesByRange, responseType: () => ssz.altair.LightClientUpdate, renderRequestBody: (req) => `${req.startPeriod},${req.count}`, contextBytes: getContextBytesLightclient((update) => modules.config.getForkName(update.signatureSlot), modules), - isSingleResponse: true, }; }; diff --git a/packages/reqresp/src/messages/Metadata.ts b/packages/reqresp/src/messages/Metadata.ts new file mode 100644 index 000000000000..846ce31238e5 --- /dev/null +++ b/packages/reqresp/src/messages/Metadata.ts @@ -0,0 +1,15 @@ +import {allForks, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Metadata: ProtocolDefinitionGenerator = (modules, handler) => { + return { + method: "metadata", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => null, + responseType: () => ssz.phase0.Metadata, + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/MetadataV2.ts b/packages/reqresp/src/messages/MetadataV2.ts new file mode 100644 index 000000000000..3d48a362d7c4 --- /dev/null +++ b/packages/reqresp/src/messages/MetadataV2.ts @@ -0,0 +1,15 @@ +import {allForks, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const MetadataV2: ProtocolDefinitionGenerator = (modules, handler) => { + return { + method: "metadata", + version: 2, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => null, + responseType: () => ssz.altair.Metadata, + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/Ping.ts b/packages/reqresp/src/messages/Ping.ts new file mode 100644 index 000000000000..3f26da6a5f0f --- /dev/null +++ b/packages/reqresp/src/messages/Ping.ts @@ -0,0 +1,16 @@ +import {phase0, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Ping: ProtocolDefinitionGenerator = (modules, handler) => { + return { + method: "ping", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => ssz.phase0.Ping, + responseType: () => ssz.phase0.Ping, + renderRequestBody: (req) => req.toString(10), + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/Status.ts b/packages/reqresp/src/messages/Status.ts new file mode 100644 index 000000000000..6e368f468832 --- /dev/null +++ b/packages/reqresp/src/messages/Status.ts @@ -0,0 +1,15 @@ +import {phase0, ssz} from "@lodestar/types"; +import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Status: ProtocolDefinitionGenerator = (_modules, handler) => { + return { + method: "status", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + handler, + requestType: () => ssz.phase0.Status, + responseType: () => ssz.phase0.Status, + contextBytes: {type: ContextBytesType.Empty}, + }; +}; diff --git a/packages/reqresp/src/messages/index.ts b/packages/reqresp/src/messages/index.ts index 1e17721f08ae..c1f6aeddff42 100644 --- a/packages/reqresp/src/messages/index.ts +++ b/packages/reqresp/src/messages/index.ts @@ -1,34 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {BeaconBlocksByRoot} from "./v1/BeaconBlocksByRoot.js"; -import {BeaconBlocksByRange} from "./v1/BeaconBlocksByRange.js"; -import {Goodbye} from "./v1/Goodbye.js"; -import {LightClientBootstrap} from "./v1/LightClientBootstrap.js"; -import {LightClientFinalityUpdate} from "./v1/LightClientFinalityUpdate.js"; -import {LightClientOptimisticUpdate} from "./v1/LightClientOptimisticUpdate.js"; -import {LightClientUpdatesByRange} from "./v1/LightClientUpdatesByRange.js"; -import {Metadata} from "./v1/Metadata.js"; -import {Ping} from "./v1/Ping.js"; -import {Status} from "./v1/Status.js"; -import {BeaconBlocksByRangeV2} from "./v2/BeaconBlocksByRange.js"; -import {BeaconBlocksByRootV2} from "./v2/BeaconBlocksByRoot.js"; -import {MetadataV2} from "./v2/Metadata.js"; - -export default { - v1: { - BeaconBlocksByRoot, - BeaconBlocksByRange, - Goodbye, - LightClientBootstrap, - LightClientFinalityUpdate, - LightClientOptimisticUpdate, - LightClientUpdatesByRange, - Metadata, - Ping, - Status, - }, - v2: { - BeaconBlocksByRange: BeaconBlocksByRangeV2, - BeaconBlocksByRoot: BeaconBlocksByRootV2, - Metadata: MetadataV2, - }, -}; +export {BeaconBlocksByRoot} from "./BeaconBlocksByRoot.js"; +export {BeaconBlocksByRootV2} from "./BeaconBlocksByRootV2.js"; +export {BeaconBlocksByRange} from "./BeaconBlocksByRange.js"; +export {BeaconBlocksByRangeV2} from "./BeaconBlocksByRangeV2.js"; +export {Goodbye} from "./Goodbye.js"; +export {LightClientBootstrap} from "./LightClientBootstrap.js"; +export {LightClientFinalityUpdate} from "./LightClientFinalityUpdate.js"; +export {LightClientOptimisticUpdate} from "./LightClientOptimisticUpdate.js"; +export {LightClientUpdatesByRange} from "./LightClientUpdatesByRange.js"; +export {Metadata} from "./Metadata.js"; +export {MetadataV2} from "./MetadataV2.js"; +export {Ping} from "./Ping.js"; +export {Status} from "./Status.js"; diff --git a/packages/reqresp/src/messages/utils.ts b/packages/reqresp/src/messages/utils.ts index 4ad874501057..2801511a0d2a 100644 --- a/packages/reqresp/src/messages/utils.ts +++ b/packages/reqresp/src/messages/utils.ts @@ -12,6 +12,3 @@ export function getContextBytesLightclient( forkFromResponse, }; } - -export const getHandlerRequiredErrorFor = (method: string): Error => - new Error(`Handler is required for method "${method}."`); diff --git a/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts deleted file mode 100644 index fe2faeb1918e..000000000000 --- a/packages/reqresp/src/messages/v1/BeaconBlocksByRange.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {allForks, phase0, ssz} from "@lodestar/types"; -import {RespStatus} from "../../interface.js"; -import {ResponseError} from "../../response/errors.js"; -import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getHandlerRequiredErrorFor} from "../utils.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const BeaconBlocksByRange: ProtocolDefinitionGenerator< - phase0.BeaconBlocksByRangeRequest, - allForks.SignedBeaconBlock -> = (_modules, handler) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRange); - } - - return { - method: Method.BeaconBlocksByRange, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* beaconBlocksByRangeHandler(context, req, peerId) { - if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - - yield* handler(req, peerId); - }, - requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, - responseType: (forkName) => ssz[forkName].SignedBeaconBlock, - renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: false, - }; -}; diff --git a/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts b/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts deleted file mode 100644 index da19028d5ffb..000000000000 --- a/packages/reqresp/src/messages/v1/BeaconBlocksByRoot.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {allForks, phase0, ssz} from "@lodestar/types"; -import {toHex} from "@lodestar/utils"; -import {RespStatus} from "../../interface.js"; -import {ResponseError} from "../../response/errors.js"; -import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getHandlerRequiredErrorFor} from "../utils.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const BeaconBlocksByRoot: ProtocolDefinitionGenerator< - phase0.BeaconBlocksByRootRequest, - allForks.SignedBeaconBlock -> = (_modules, handler) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRoot); - } - - return { - method: Method.BeaconBlocksByRoot, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* beaconBlocksByRootHandler(context, req, peerId) { - if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - - yield* handler(req, peerId); - }, - requestType: () => ssz.phase0.BeaconBlocksByRootRequest, - responseType: (forkName) => ssz[forkName].SignedBeaconBlock, - renderRequestBody: (req) => req.map((root) => toHex(root)).join(","), - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: false, - }; -}; diff --git a/packages/reqresp/src/messages/v1/Goodbye.ts b/packages/reqresp/src/messages/v1/Goodbye.ts deleted file mode 100644 index beff4fa20d47..000000000000 --- a/packages/reqresp/src/messages/v1/Goodbye.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {phase0, ssz} from "@lodestar/types"; -import { - ContextBytesType, - EncodedPayloadType, - Encoding, - Method, - ProtocolDefinitionGenerator, - Version, -} from "../../types.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const Goodbye: ProtocolDefinitionGenerator = (_handler) => { - return { - method: Method.Status, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* goodbyeHandler(context, req, peerId) { - context.eventHandlers.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); - - yield {type: EncodedPayloadType.ssz, data: context.modules.metadataController.seqNumber}; - }, - requestType: () => ssz.phase0.Goodbye, - responseType: () => ssz.phase0.Goodbye, - renderRequestBody: (req) => req.toString(10), - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }; -}; diff --git a/packages/reqresp/src/messages/v1/Metadata.ts b/packages/reqresp/src/messages/v1/Metadata.ts deleted file mode 100644 index e5ddda0df86f..000000000000 --- a/packages/reqresp/src/messages/v1/Metadata.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {allForks, ssz} from "@lodestar/types"; -import { - ContextBytesType, - EncodedPayloadType, - Encoding, - Method, - ProtocolDefinitionGenerator, - Version, -} from "../../types.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const Metadata: ProtocolDefinitionGenerator = (modules) => { - return { - method: Method.Metadata, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* metadataHandler(context, req, peerId) { - context.eventHandlers.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); - - yield {type: EncodedPayloadType.ssz, data: modules.metadataController.json}; - }, - requestType: () => null, - responseType: () => ssz.phase0.Metadata, - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }; -}; diff --git a/packages/reqresp/src/messages/v1/Ping.ts b/packages/reqresp/src/messages/v1/Ping.ts deleted file mode 100644 index 009aa5dca042..000000000000 --- a/packages/reqresp/src/messages/v1/Ping.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {phase0, ssz} from "@lodestar/types"; -import { - ContextBytesType, - EncodedPayloadType, - Encoding, - Method, - ProtocolDefinitionGenerator, - Version, -} from "../../types.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const Ping: ProtocolDefinitionGenerator = (modules) => { - return { - method: Method.Status, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* pingHandler(context, req, peerId) { - context.eventHandlers.onIncomingRequestBody({method: Method.Ping, body: req}, peerId); - - yield {type: EncodedPayloadType.ssz, data: modules.metadataController.seqNumber}; - }, - requestType: () => ssz.phase0.Ping, - responseType: () => ssz.phase0.Ping, - renderRequestBody: (req) => req.toString(10), - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }; -}; diff --git a/packages/reqresp/src/messages/v1/Status.ts b/packages/reqresp/src/messages/v1/Status.ts deleted file mode 100644 index 0d204702a9c8..000000000000 --- a/packages/reqresp/src/messages/v1/Status.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {phase0, ssz} from "@lodestar/types"; -import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getHandlerRequiredErrorFor} from "../utils.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const Status: ProtocolDefinitionGenerator = (_modules, handler) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.Status); - } - - return { - method: Method.Status, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* statusHandler(context, req, peerId) { - context.eventHandlers.onIncomingRequestBody({method: Method.Status, body: req}, peerId); - - yield* handler(req, peerId); - }, - requestType: () => ssz.phase0.Status, - responseType: () => ssz.phase0.Status, - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }; -}; diff --git a/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts b/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts deleted file mode 100644 index 0df2ffdaadb6..000000000000 --- a/packages/reqresp/src/messages/v2/BeaconBlocksByRange.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {allForks, phase0, ssz} from "@lodestar/types"; -import {RespStatus} from "../../interface.js"; -import {ResponseError} from "../../response/errors.js"; -import {ContextBytesType, Encoding, Method, ProtocolDefinitionGenerator, Version} from "../../types.js"; -import {getHandlerRequiredErrorFor} from "../utils.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const BeaconBlocksByRangeV2: ProtocolDefinitionGenerator< - phase0.BeaconBlocksByRangeRequest, - allForks.SignedBeaconBlock -> = (modules, handler) => { - if (!handler) { - throw getHandlerRequiredErrorFor(Method.BeaconBlocksByRange); - } - - return { - method: Method.BeaconBlocksByRange, - version: Version.V2, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* beaconBlocksByRangeV2Handler(context, req, peerId) { - if (!context.modules.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - - yield* handler(req, peerId); - }, - requestType: () => ssz.phase0.BeaconBlocksByRangeRequest, - responseType: (forkName) => ssz[forkName].SignedBeaconBlock, - renderRequestBody: (req) => `${req.startSlot},${req.step},${req.count}`, - contextBytes: { - type: ContextBytesType.ForkDigest, - forkDigestContext: modules.config, - forkFromResponse: (block) => modules.config.getForkName(block.message.slot), - }, - isSingleResponse: false, - }; -}; diff --git a/packages/reqresp/src/messages/v2/Metadata.ts b/packages/reqresp/src/messages/v2/Metadata.ts deleted file mode 100644 index 4dfde9756b25..000000000000 --- a/packages/reqresp/src/messages/v2/Metadata.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {allForks, ssz} from "@lodestar/types"; -import { - ContextBytesType, - EncodedPayloadType, - Encoding, - Method, - ProtocolDefinitionGenerator, - Version, -} from "../../types.js"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const MetadataV2: ProtocolDefinitionGenerator = (modules) => { - return { - method: Method.Metadata, - version: Version.V2, - encoding: Encoding.SSZ_SNAPPY, - handler: async function* metadataV2Handler(context, req, peerId) { - context.eventHandlers.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); - - yield {type: EncodedPayloadType.ssz, data: modules.metadataController.json}; - }, - requestType: () => null, - responseType: () => ssz.altair.Metadata, - contextBytes: {type: ContextBytesType.Empty}, - isSingleResponse: true, - }; -}; diff --git a/packages/reqresp/src/metrics.ts b/packages/reqresp/src/metrics.ts index 5f09721744bd..d9683b84ed92 100644 --- a/packages/reqresp/src/metrics.ts +++ b/packages/reqresp/src/metrics.ts @@ -29,12 +29,6 @@ interface Histogram { reset(): void; } -interface AvgMinMax { - set(values: number[]): void; - set(labels: Labels, values: number[]): void; - set(arg1?: Labels | number[], arg2?: number[]): void; -} - type GaugeConfig = { name: string; help: string; @@ -48,12 +42,9 @@ type HistogramConfig = { buckets?: number[]; }; -type AvgMinMaxConfig = GaugeConfig; - export interface MetricsRegister { gauge(config: GaugeConfig): Gauge; histogram(config: HistogramConfig): Histogram; - avgMinMax(config: AvgMinMaxConfig): AvgMinMax; } export type Metrics = ReturnType; @@ -71,18 +62,9 @@ export type LodestarGitData = { * A collection of metrics used throughout the Gossipsub behaviour. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type -export function getMetrics(register: MetricsRegister, gitData: LodestarGitData) { +export function getMetrics(register: MetricsRegister) { // Using function style instead of class to prevent having to re-declare all MetricsPrometheus types. - // Track version, same as https://github.com/ChainSafe/lodestar/blob/6df28de64f12ea90b341b219229a47c8a25c9343/packages/lodestar/src/metrics/metrics/lodestar.ts#L17 - register - .gauge({ - name: "lodestar_version", - help: "Lodestar version", - labelNames: Object.keys(gitData) as (keyof LodestarGitData)[], - }) - .set(gitData, 1); - return { outgoingRequests: register.gauge<{method: string}>({ name: "beacon_reqresp_outgoing_requests_total", diff --git a/packages/reqresp/src/rate_limiter/RateLimiter.ts b/packages/reqresp/src/rate_limiter/RateLimiter.ts index 0f45e6cac205..54cc10a32f74 100644 --- a/packages/reqresp/src/rate_limiter/RateLimiter.ts +++ b/packages/reqresp/src/rate_limiter/RateLimiter.ts @@ -2,12 +2,11 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ILogger, MapDef} from "@lodestar/utils"; import {RateLimiter} from "../interface.js"; import {Metrics} from "../metrics.js"; -import {IPeerRpcScoreStore, PeerAction} from "../sharedTypes.js"; import {RateTracker} from "./RateTracker.js"; interface RateLimiterModules { logger: ILogger; - peerRpcScores: IPeerRpcScoreStore; + reportPeer: (peer: PeerId) => void; metrics: Metrics | null; } @@ -37,7 +36,7 @@ const DISCONNECTED_TIMEOUT_MS = 5 * 60 * 1000; */ export class InboundRateLimiter implements RateLimiter { private readonly logger: ILogger; - private readonly peerRpcScores: IPeerRpcScoreStore; + private readonly reportPeer: RateLimiterModules["reportPeer"]; private readonly metrics: Metrics | null; private requestCountTrackersByPeer: MapDef; /** @@ -68,6 +67,7 @@ export class InboundRateLimiter implements RateLimiter { constructor(options: Partial, modules: RateLimiterModules) { this.options = {...InboundRateLimiter.defaults, ...options}; + this.reportPeer = modules.reportPeer; this.requestCountTrackersByPeer = new MapDef( () => new RateTracker({limit: this.options.requestCountPeerLimit, timeoutMs: this.options.rateTrackerTimeoutMs}) @@ -80,7 +80,6 @@ export class InboundRateLimiter implements RateLimiter { () => new RateTracker({limit: this.options.blockCountPeerLimit, timeoutMs: this.options.rateTrackerTimeoutMs}) ); this.logger = modules.logger; - this.peerRpcScores = modules.peerRpcScores; this.metrics = modules.metrics; this.lastSeenRequestsByPeer = new Map(); } @@ -109,7 +108,7 @@ export class InboundRateLimiter implements RateLimiter { peerId: peerIdStr, requestsWithinWindow: requestCountPeerTracker.getRequestedObjectsWithinWindow(), }); - this.peerRpcScores.applyAction(peerId, PeerAction.Fatal, "RateLimit"); + this.reportPeer(peerId); if (this.metrics) { this.metrics.rateLimitErrors.inc({tracker: "requestCountPeerTracker"}); } @@ -132,7 +131,7 @@ export class InboundRateLimiter implements RateLimiter { blockCount: numBlock, requestsWithinWindow: blockCountPeerTracker.getRequestedObjectsWithinWindow(), }); - this.peerRpcScores.applyAction(peerId, PeerAction.Fatal, "RateLimit"); + this.reportPeer(peerId); if (this.metrics) { this.metrics.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); } diff --git a/packages/reqresp/src/request/collectResponses.ts b/packages/reqresp/src/request/collectResponses.ts deleted file mode 100644 index 018354390f87..000000000000 --- a/packages/reqresp/src/request/collectResponses.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {ProtocolDefinition} from "../types.js"; -import {RequestErrorCode, RequestInternalError} from "./errors.js"; - -/** - * Sink for `*`, from - * ```bnf - * response ::= * - * ``` - * Note: `response` has zero or more chunks for SSZ-list responses or exactly one chunk for non-list - */ -export function collectResponses( - protocol: ProtocolDefinition, - maxResponses?: number -): (source: AsyncIterable) => Promise { - return async (source) => { - if (protocol.isSingleResponse) { - for await (const response of source) { - return response; - } - throw new RequestInternalError({code: RequestErrorCode.EMPTY_RESPONSE}); - } - - // else: zero or more responses - const responses: T[] = []; - for await (const response of source) { - responses.push(response); - - if (maxResponses !== undefined && responses.length >= maxResponses) { - break; - } - } - return responses; - }; -} diff --git a/packages/reqresp/src/request/errors.ts b/packages/reqresp/src/request/errors.ts index ce9b2fcacdb6..da131686c8ae 100644 --- a/packages/reqresp/src/request/errors.ts +++ b/packages/reqresp/src/request/errors.ts @@ -1,5 +1,5 @@ import {LodestarError} from "@lodestar/utils"; -import {Method, Encoding} from "../types.js"; +import {Encoding} from "../types.js"; import {ResponseError} from "../response/index.js"; import {RespStatus, RpcResponseStatusError} from "../interface.js"; @@ -46,7 +46,7 @@ type RequestErrorType = | {code: RequestErrorCode.RESP_TIMEOUT}; export interface IRequestErrorMetadata { - method: Method; + method: string; encoding: Encoding; peer: string; // Do not include requestId in error metadata to make the errors deterministic for tests diff --git a/packages/reqresp/src/request/index.ts b/packages/reqresp/src/request/index.ts index 7562d282079d..8311af70348e 100644 --- a/packages/reqresp/src/request/index.ts +++ b/packages/reqresp/src/request/index.ts @@ -4,12 +4,10 @@ import {Libp2p} from "libp2p"; import {Uint8ArrayList} from "uint8arraylist"; import {ErrorAborted, ILogger, withTimeout, TimeoutError} from "@lodestar/utils"; import {ProtocolDefinition} from "../types.js"; -import {formatProtocolID, prettyPrintPeerId, abortableSource} from "../utils/index.js"; +import {prettyPrintPeerId, abortableSource} from "../utils/index.js"; import {ResponseError} from "../response/index.js"; import {requestEncode} from "../encoders/requestEncode.js"; import {responseDecode} from "../encoders/responseDecode.js"; -import {timeoutOptions} from "../constants.js"; -import {collectResponses} from "./collectResponses.js"; import { RequestError, RequestErrorCode, @@ -20,6 +18,23 @@ import { export {RequestError, RequestErrorCode}; +// Default spec values from https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#configuration +export const DEFAULT_DIAL_TIMEOUT = 5 * 1000; // 5 sec +export const DEFAULT_REQUEST_TIMEOUT = 5 * 1000; // 5 sec +export const DEFAULT_TTFB_TIMEOUT = 5 * 1000; // 5 sec +export const DEFAULT_RESP_TIMEOUT = 10 * 1000; // 10 sec + +export interface SendRequestOpts { + /** The maximum time for complete response transfer. */ + respTimeoutMs?: number; + /** Non-spec timeout from sending request until write stream closed by responder */ + requestTimeoutMs?: number; + /** The maximum time to wait for first byte of request response (time-to-first-byte). */ + ttfbTimeoutMs?: number; + /** Non-spec timeout from dialing protocol until stream opened */ + dialTimeoutMs?: number; +} + type SendRequestModules = { logger: ILogger; libp2p: Libp2p; @@ -37,21 +52,25 @@ type SendRequestModules = { * - Any part of the response_chunk fails validation. Throws a typed error (see `SszSnappyError`) * - The maximum number of requested chunks are read. Does not throw, returns read chunks only. */ -export async function sendRequest( +export async function* sendRequest( {logger, libp2p, peerClient}: SendRequestModules, peerId: PeerId, protocols: ProtocolDefinition[], + protocolIDs: string[], requestBody: Req, - maxResponses: number, signal?: AbortSignal, - options?: Partial, + opts?: SendRequestOpts, requestId = 0 -): Promise { +): AsyncIterable { if (protocols.length === 0) { throw Error("sendRequest must set > 0 protocols"); } - const {REQUEST_TIMEOUT, DIAL_TIMEOUT} = {...timeoutOptions, ...options}; + const DIAL_TIMEOUT = opts?.dialTimeoutMs ?? DEFAULT_DIAL_TIMEOUT; + const REQUEST_TIMEOUT = opts?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT; + const TTFB_TIMEOUT = opts?.ttfbTimeoutMs ?? DEFAULT_TTFB_TIMEOUT; + const RESP_TIMEOUT = opts?.respTimeoutMs ?? DEFAULT_RESP_TIMEOUT; + const peerIdStr = peerId.toString(); const peerIdStrShort = prettyPrintPeerId(peerId); const {method, encoding} = protocols[0]; @@ -68,7 +87,7 @@ export async function sendRequest( // On stream negotiation `libp2p.dialProtocol` will pick the available protocol and return // the picked protocol in `connection.protocol` const protocolsMap = new Map( - protocols.map((protocol) => [formatProtocolID(protocol.method, protocol.version, protocol.encoding), protocol]) + protocols.map((protocol, i) => [protocolIDs[i], protocol]) ); // As of October 2020 we can't rely on libp2p.dialProtocol timeout to work so @@ -127,19 +146,15 @@ export async function sendRequest( logger.debug("Req request sent", logCtx); - const {TTFB_TIMEOUT, RESP_TIMEOUT} = {...timeoutOptions, ...options}; - // - TTFB_TIMEOUT: The requester MUST wait a maximum of TTFB_TIMEOUT for the first response byte to arrive // - RESP_TIMEOUT: Requester allows a further RESP_TIMEOUT for each subsequent response_chunk // - Max total timeout: This timeout is not required by the spec. It may not be necessary, but it's kept as // safe-guard to close. streams in case of bugs on other timeout mechanisms. const ttfbTimeoutController = new AbortController(); const respTimeoutController = new AbortController(); - const maxRTimeoutController = new AbortController(); const timeoutTTFB = setTimeout(() => ttfbTimeoutController.abort(), TTFB_TIMEOUT); let timeoutRESP: NodeJS.Timeout | null = null; - const timeoutMaxR = setTimeout(() => maxRTimeoutController.abort(), TTFB_TIMEOUT + maxResponses * RESP_TIMEOUT); const restartRespTimeout = (): void => { if (timeoutRESP) clearTimeout(timeoutRESP); @@ -148,7 +163,7 @@ export async function sendRequest( try { // Note: libp2p.stop() will close all connections, so not necessary to abort this pipe on parent stop - const responses = await pipe( + yield* pipe( abortableSource(stream.source as AsyncIterable, [ { signal: ttfbTimeoutController.signal, @@ -158,10 +173,6 @@ export async function sendRequest( signal: respTimeoutController.signal, getError: () => new RequestInternalError({code: RequestErrorCode.RESP_TIMEOUT}), }, - { - signal: maxRTimeoutController.signal, - getError: () => new RequestInternalError({code: RequestErrorCode.RESPONSE_TIMEOUT}), - }, ]), // Transforms `Buffer` chunks to yield `ResponseBody` chunks @@ -175,22 +186,16 @@ export async function sendRequest( // On , cancel this chunk's RESP_TIMEOUT and start next's restartRespTimeout(); }, - }), - - collectResponses(protocol, maxResponses) + }) ); // NOTE: Only log once per request to verbose, intermediate steps to debug // NOTE: Do not log the response, logs get extremely cluttered // NOTE: add double space after "Req " to align log with the "Resp " log - const numResponse = Array.isArray(responses) ? responses.length : 1; - logger.verbose("Req done", {...logCtx, numResponse}); - - return responses as Resp; + logger.verbose("Req done", {...logCtx}); } finally { clearTimeout(timeoutTTFB); if (timeoutRESP !== null) clearTimeout(timeoutRESP); - clearTimeout(timeoutMaxR); // Necessary to call `stream.close()` since collectResponses() may break out of the source before exhausting it // `stream.close()` libp2p-mplex will .end() the source (it-pushable instance) diff --git a/packages/reqresp/src/response/index.ts b/packages/reqresp/src/response/index.ts index f01ffce45d18..64315875751f 100644 --- a/packages/reqresp/src/response/index.ts +++ b/packages/reqresp/src/response/index.ts @@ -3,18 +3,19 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {Stream} from "@libp2p/interface-connection"; import {Uint8ArrayList} from "uint8arraylist"; import {ILogger, TimeoutError, withTimeout} from "@lodestar/utils"; -import {REQUEST_TIMEOUT} from "../constants.js"; import {prettyPrintPeerId} from "../utils/index.js"; import {ProtocolDefinition} from "../types.js"; import {requestDecode} from "../encoders/requestDecode.js"; import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js"; -import {ReqRespHandlerContext, ReqRespHandlerProtocolContext, RespStatus} from "../interface.js"; +import {RespStatus} from "../interface.js"; import {ResponseError} from "./errors.js"; export {ResponseError}; -export interface HandleRequestOpts { - context: Context; +// Default spec values from https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#configuration +export const DEFAULT_REQUEST_TIMEOUT = 5 * 1000; // 5 sec + +export interface HandleRequestOpts { logger: ILogger; stream: Stream; peerId: PeerId; @@ -23,6 +24,8 @@ export interface HandleRequestOpts { requestId?: number; /** Peer client type for logging and metrics: 'prysm' | 'lighthouse' */ peerClient?: string; + /** Non-spec timeout from sending request until write stream closed by responder */ + requestTimeoutMs?: number; } /** @@ -35,8 +38,7 @@ export interface HandleRequestOpts { * 4a. Encode and write `` to peer * 4b. On error, encode and write an error `` and stop */ -export async function handleRequest({ - context, +export async function handleRequest({ logger, stream, peerId, @@ -44,7 +46,10 @@ export async function handleRequest): Promise { + requestTimeoutMs, +}: HandleRequestOpts): Promise { + const REQUEST_TIMEOUT = requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT; + const logCtx = {method: protocol.method, client: peerClient, peer: prettyPrintPeerId(peerId), requestId}; let responseError: Error | null = null; @@ -70,7 +75,7 @@ export async function handleRequest logger.debug("Resp sending chunk", logCtx)), diff --git a/packages/reqresp/src/sharedTypes.ts b/packages/reqresp/src/sharedTypes.ts index 50be4ec9ac79..73255bbbeb26 100644 --- a/packages/reqresp/src/sharedTypes.ts +++ b/packages/reqresp/src/sharedTypes.ts @@ -1,11 +1,4 @@ -import {EventEmitter} from "events"; -import {PeerId} from "@libp2p/interface-peer-id"; -import StrictEventEmitter from "strict-event-emitter-types"; -import {ENR} from "@chainsafe/discv5"; -import {BitArray} from "@chainsafe/ssz"; -import {ForkName} from "@lodestar/params"; -import {allForks, altair, Epoch, phase0} from "@lodestar/types"; -import {Encoding, RequestTypedContainer} from "./types.js"; +import {Encoding} from "./types.js"; // These interfaces are shared among beacon-node package. export enum ScoreState { @@ -17,75 +10,12 @@ export enum ScoreState { Banned = "Banned", } -type PeerIdStr = string; - -export enum PeerAction { - /** Immediately ban peer */ - Fatal = "Fatal", - /** - * Not malicious action, but it must not be tolerated - * ~5 occurrences will get the peer banned - */ - LowToleranceError = "LowToleranceError", - /** - * Negative action that can be tolerated only sometimes - * ~10 occurrences will get the peer banned - */ - MidToleranceError = "MidToleranceError", - /** - * Some error that can be tolerated multiple times - * ~50 occurrences will get the peer banned - */ - HighToleranceError = "HighToleranceError", -} - -export interface IPeerRpcScoreStore { - getScore(peer: PeerId): number; - getScoreState(peer: PeerId): ScoreState; - applyAction(peer: PeerId, action: PeerAction, actionName: string): void; - update(): void; - updateGossipsubScore(peerId: PeerIdStr, newScore: number, ignore: boolean): void; -} - -export enum NetworkEvent { - /** A relevant peer has connected or has been re-STATUS'd */ - peerConnected = "peer-manager.peer-connected", - peerDisconnected = "peer-manager.peer-disconnected", - gossipStart = "gossip.start", - gossipStop = "gossip.stop", - gossipHeartbeat = "gossipsub.heartbeat", - reqRespRequest = "req-resp.request", - unknownBlockParent = "unknownBlockParent", -} - -export type NetworkEvents = { - [NetworkEvent.peerConnected]: (peer: PeerId, status: phase0.Status) => void; - [NetworkEvent.peerDisconnected]: (peer: PeerId) => void; - [NetworkEvent.reqRespRequest]: (request: RequestTypedContainer, peer: PeerId) => void; - [NetworkEvent.unknownBlockParent]: (signedBlock: allForks.SignedBeaconBlock, peerIdStr: string) => void; -}; - -export type INetworkEventBus = StrictEventEmitter; - export enum RelevantPeerStatus { Unknown = "unknown", relevant = "relevant", irrelevant = "irrelevant", } -export type PeerData = { - lastReceivedMsgUnixTsMs: number; - lastStatusUnixTsMs: number; - connectedUnixTsMs: number; - relevantStatus: RelevantPeerStatus; - direction: "inbound" | "outbound"; - peerId: PeerId; - metadata: altair.Metadata | null; - agentVersion: string | null; - agentClient: ClientKind | null; - encodingPreference: Encoding | null; -}; - export enum ClientKind { Lighthouse = "Lighthouse", Nimbus = "Nimbus", @@ -101,12 +31,3 @@ export interface PeersData { getEncodingPreference(peerIdStr: string): Encoding | null; setEncodingPreference(peerIdStr: string, encoding: Encoding): void; } - -export interface MetadataController { - seqNumber: bigint; - syncnets: BitArray; - attnets: BitArray; - json: altair.Metadata; - start(enr: ENR | undefined, currentFork: ForkName): void; - updateEth2Field(epoch: Epoch): void; -} diff --git a/packages/reqresp/src/types.ts b/packages/reqresp/src/types.ts index 0098d303e9c2..7ce98774fd40 100644 --- a/packages/reqresp/src/types.ts +++ b/packages/reqresp/src/types.ts @@ -1,11 +1,9 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {Type} from "@chainsafe/ssz"; -import {IForkConfig, IForkDigestContext} from "@lodestar/config"; +import {IBeaconConfig, IForkConfig, IForkDigestContext} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; -import {phase0, Slot} from "@lodestar/types"; +import {Slot} from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; -import {timeoutOptions} from "./constants.js"; -import {ReqRespHandlerContext, ReqRespHandlerProtocolContext} from "./interface.js"; export enum EncodedPayloadType { ssz, @@ -23,38 +21,28 @@ export type EncodedPayload = contextBytes: ContextBytes; }; -export type ReqRespHandlerWithContext< - Req, - Resp, - Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext -> = (context: Context, req: Req, peerId: PeerId) => AsyncIterable>; - export type ReqRespHandler = (req: Req, peerId: PeerId) => AsyncIterable>; -export interface ProtocolDefinition< - Req = unknown, - Resp = unknown, - Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext -> extends Protocol { - handler: ReqRespHandlerWithContext; +export interface ProtocolDefinition { + /** Protocol name identifier `beacon_blocks_by_range` or `status` */ + method: string; + /** Version counter: `1`, `2` etc */ + version: number; + encoding: Encoding; + handler: ReqRespHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any requestType: (fork: ForkName) => Type | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any responseType: (fork: ForkName) => Type; renderRequestBody?: (request: Req) => string; contextBytes: ContextBytesFactory; - isSingleResponse: boolean; } -export type ProtocolDefinitionGenerator< - Req, - Res, - Context extends ReqRespHandlerProtocolContext = ReqRespHandlerContext -> = ( +export type ProtocolDefinitionGenerator = ( // "inboundRateLimiter" is available only on handler context not on generator - modules: Omit, - handler?: ReqRespHandler -) => ProtocolDefinition; + modules: {config: IBeaconConfig}, + handler: ReqRespHandler +) => ProtocolDefinition; export type HandlerTypeFromMessage = T extends ProtocolDefinitionGenerator ? ReqRespHandler @@ -62,46 +50,6 @@ export type HandlerTypeFromMessage = T extends ProtocolDefinitionGenerator = @@ -135,8 +77,6 @@ export enum ContextBytesType { ForkDigest, } -export type ReqRespOptions = typeof timeoutOptions; - export enum LightClientServerErrorCode { RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE", } diff --git a/packages/reqresp/src/utils/assertSequentialBlocksInRange.ts b/packages/reqresp/src/utils/assertSequentialBlocksInRange.ts deleted file mode 100644 index a532bf15a4e5..000000000000 --- a/packages/reqresp/src/utils/assertSequentialBlocksInRange.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {allForks, phase0} from "@lodestar/types"; -import {LodestarError} from "@lodestar/utils"; - -/** - * Asserts a response from BeaconBlocksByRange respects the request and is sequential - * Note: MUST allow missing block for skipped slots. - */ -export function assertSequentialBlocksInRange( - blocks: allForks.SignedBeaconBlock[], - {count, startSlot, step}: phase0.BeaconBlocksByRangeRequest -): void { - // Check below would throw for empty ranges - if (blocks.length === 0) { - return; - } - - const length = blocks.length; - if (length > count) { - throw new BlocksByRangeError({code: BlocksByRangeErrorCode.BAD_LENGTH, count, length}); - } - - const maxSlot = startSlot + count * (step || 1) - 1; - const firstSlot = blocks[0].message.slot; - const lastSlot = blocks[blocks.length - 1].message.slot; - - if (firstSlot < startSlot) { - throw new BlocksByRangeError({code: BlocksByRangeErrorCode.UNDER_START_SLOT, startSlot, firstSlot}); - } - - if (lastSlot > maxSlot) { - throw new BlocksByRangeError({code: BlocksByRangeErrorCode.OVER_MAX_SLOT, maxSlot, lastSlot}); - } - - // Assert sequential with request.step - for (let i = 0; i < blocks.length - 1; i++) { - const slotL = blocks[i].message.slot; - const slotR = blocks[i + 1].message.slot; - if (slotL + step > slotR) { - throw new BlocksByRangeError({code: BlocksByRangeErrorCode.BAD_SEQUENCE, step, slotL, slotR}); - } - } -} - -export enum BlocksByRangeErrorCode { - BAD_LENGTH = "BLOCKS_BY_RANGE_ERROR_BAD_LENGTH", - UNDER_START_SLOT = "BLOCKS_BY_RANGE_ERROR_UNDER_START_SLOT", - OVER_MAX_SLOT = "BLOCKS_BY_RANGE_ERROR_OVER_MAX_SLOT", - BAD_SEQUENCE = "BLOCKS_BY_RANGE_ERROR_BAD_SEQUENCE", -} - -type BlocksByRangeErrorType = - | {code: BlocksByRangeErrorCode.BAD_LENGTH; count: number; length: number} - | {code: BlocksByRangeErrorCode.UNDER_START_SLOT; startSlot: number; firstSlot: number} - | {code: BlocksByRangeErrorCode.OVER_MAX_SLOT; maxSlot: number; lastSlot: number} - | {code: BlocksByRangeErrorCode.BAD_SEQUENCE; step: number; slotL: number; slotR: number}; - -export class BlocksByRangeError extends LodestarError {} diff --git a/packages/reqresp/src/utils/collectExactOne.ts b/packages/reqresp/src/utils/collectExactOne.ts new file mode 100644 index 000000000000..6575c1034ac2 --- /dev/null +++ b/packages/reqresp/src/utils/collectExactOne.ts @@ -0,0 +1,15 @@ +import {RequestErrorCode, RequestInternalError} from "../request/errors.js"; + +/** + * Sink for `*`, from + * ```bnf + * response ::= * + * ``` + * Expects exactly one response + */ +export async function collectExactOne(source: AsyncIterable): Promise { + for await (const response of source) { + return response; + } + throw new RequestInternalError({code: RequestErrorCode.EMPTY_RESPONSE}); +} diff --git a/packages/reqresp/src/utils/collectMaxResponse.ts b/packages/reqresp/src/utils/collectMaxResponse.ts new file mode 100644 index 000000000000..42ce2c1c8611 --- /dev/null +++ b/packages/reqresp/src/utils/collectMaxResponse.ts @@ -0,0 +1,19 @@ +/** + * Sink for `*`, from + * ```bnf + * response ::= * + * ``` + * Collects a bounded list of responses up to `maxResponses` + */ +export async function collectMaxResponse(source: AsyncIterable, maxResponses: number): Promise { + // else: zero or more responses + const responses: T[] = []; + for await (const response of source) { + responses.push(response); + + if (maxResponses !== undefined && responses.length >= maxResponses) { + break; + } + } + return responses; +} diff --git a/packages/reqresp/src/utils/index.ts b/packages/reqresp/src/utils/index.ts index daabe0458abc..b454e4695673 100644 --- a/packages/reqresp/src/utils/index.ts +++ b/packages/reqresp/src/utils/index.ts @@ -1,8 +1,7 @@ -export * from "./assertSequentialBlocksInRange.js"; +export * from "./abortableSource.js"; export * from "./bufferedSource.js"; +export * from "./collectExactOne.js"; +export * from "./collectMaxResponse.js"; export * from "./errorMessage.js"; export * from "./onChunk.js"; -export * from "./protocolId.js"; export * from "./peerId.js"; -export * from "./abortableSource.js"; -export * from "./multifork.js"; diff --git a/packages/reqresp/src/utils/multifork.ts b/packages/reqresp/src/utils/multifork.ts deleted file mode 100644 index dccec9a90821..000000000000 --- a/packages/reqresp/src/utils/multifork.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Slot} from "@lodestar/types"; -import {bytesToInt} from "@lodestar/utils"; - -const SLOT_BYTES_POSITION_IN_BLOCK = 100; -const SLOT_BYTE_COUNT = 8; - -export function getSlotFromBytes(bytes: Buffer | Uint8Array): Slot { - return bytesToInt(bytes.subarray(SLOT_BYTES_POSITION_IN_BLOCK, SLOT_BYTES_POSITION_IN_BLOCK + SLOT_BYTE_COUNT)); -} diff --git a/packages/reqresp/src/utils/protocolId.ts b/packages/reqresp/src/utils/protocolId.ts deleted file mode 100644 index c88ba171f80e..000000000000 --- a/packages/reqresp/src/utils/protocolId.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Encoding, protocolPrefix} from "../types.js"; - -/** - * @param method `"beacon_blocks_by_range"` - * @param version `"1"` - * @param encoding `"ssz_snappy"` - */ -export function formatProtocolID(method: string, version: string, encoding: Encoding): string { - return `${protocolPrefix}/${method}/${version}/${encoding}`; -} From d3dafcc917384a280f176c96eb6e64b874677b84 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 18 Nov 2022 17:33:01 +0100 Subject: [PATCH 09/23] FIx types --- .../beacon-node/src/metrics/metrics/beacon.ts | 8 ++ packages/beacon-node/src/network/events.ts | 2 +- packages/beacon-node/src/network/interface.ts | 4 +- packages/beacon-node/src/network/network.ts | 21 ++-- packages/beacon-node/src/network/options.ts | 5 +- .../src/network/peers/peerManager.ts | 6 +- .../src/network/peers/peersData.ts | 2 +- .../src/network/reqresp/handlers/index.ts | 25 ++-- .../src/network/reqresp/handlers/onStatus.ts | 7 -- .../network/reqresp/inboundRateLimiter.ts} | 52 ++++----- .../beacon-node/src/network/reqresp/index.ts | 107 ++++++++++++------ .../src/network/reqresp/interface.ts | 2 +- .../src/network/reqresp/rateTracker.ts} | 0 .../beacon-node/src/network/reqresp/score.ts | 18 +-- .../beacon-node/src/network/reqresp/types.ts | 26 ++--- packages/beacon-node/src/node/nodejs.ts | 3 +- .../test/e2e/network/reqresp.test.ts | 16 +-- .../network/reqresp/encoders/request.test.ts | 12 +- .../reqresp/encoders/requestTypes.test.ts | 31 ++--- .../network/reqresp/encoders/response.test.ts | 30 ++--- .../reqresp/encoders/responseTypes.test.ts | 26 ++--- .../reqresp/request/collectResponses.test.ts | 8 +- .../request/responseTimeoutsHandler.test.ts | 4 +- .../network/reqresp/response/index.test.ts | 6 +- .../reqresp/response/rateLimiter.test.ts | 28 +++-- .../test/unit/network/util.test.ts | 10 +- packages/reqresp/.babel-register | 15 --- packages/reqresp/.gitignore | 10 -- packages/reqresp/.mocharc.yaml | 8 -- packages/reqresp/README.md | 26 +++-- packages/reqresp/package.json | 5 +- packages/reqresp/src/ReqResp.ts | 10 +- .../reqresp/src/encoders/responseDecode.ts | 11 +- .../reqresp/src/encodingStrategies/index.ts | 7 +- .../encodingStrategies/sszSnappy/decode.ts | 9 +- .../encodingStrategies/sszSnappy/encode.ts | 10 +- packages/reqresp/src/messages/Ping.ts | 6 +- packages/reqresp/src/rate_limiter/index.ts | 2 - packages/reqresp/src/sharedTypes.ts | 33 ------ packages/reqresp/src/types.ts | 15 ++- 40 files changed, 306 insertions(+), 320 deletions(-) delete mode 100644 packages/beacon-node/src/network/reqresp/handlers/onStatus.ts rename packages/{reqresp/src/rate_limiter/RateLimiter.ts => beacon-node/src/network/reqresp/inboundRateLimiter.ts} (78%) rename packages/{reqresp/src/rate_limiter/RateTracker.ts => beacon-node/src/network/reqresp/rateTracker.ts} (100%) delete mode 100644 packages/reqresp/.babel-register delete mode 100644 packages/reqresp/.gitignore delete mode 100644 packages/reqresp/.mocharc.yaml delete mode 100644 packages/reqresp/src/rate_limiter/index.ts delete mode 100644 packages/reqresp/src/sharedTypes.ts diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index a62b513c8283..10ae4f65972d 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -95,6 +95,14 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { buckets: [1, 2, 3, 5, 7, 10, 20, 30, 50, 100], }), + reqResp: { + rateLimitErrors: register.gauge<"tracker">({ + name: "beacon_reqresp_rate_limiter_errors_total", + help: "Count rate limiter errors", + labelNames: ["tracker"], + }), + }, + blockProductionTime: register.histogram({ name: "beacon_block_production_seconds", help: "Full runtime of block production", diff --git a/packages/beacon-node/src/network/events.ts b/packages/beacon-node/src/network/events.ts index eb4cfdd4043e..d527c1587879 100644 --- a/packages/beacon-node/src/network/events.ts +++ b/packages/beacon-node/src/network/events.ts @@ -2,7 +2,7 @@ import {EventEmitter} from "events"; import {PeerId} from "@libp2p/interface-peer-id"; import StrictEventEmitter from "strict-event-emitter-types"; import {allForks, phase0} from "@lodestar/types"; -import {RequestTypedContainer} from "@lodestar/reqresp"; +import {RequestTypedContainer} from "./reqresp/types.js"; export enum NetworkEvent { /** A relevant peer has connected or has been re-STATUS'd */ diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 3c66a1e5d7a4..db4a65d0bd15 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -2,7 +2,7 @@ import {Connection} from "@libp2p/interface-connection"; import {Multiaddr} from "@multiformats/multiaddr"; import {PeerId} from "@libp2p/interface-peer-id"; import {Discv5, ENR} from "@chainsafe/discv5"; -import {IReqResp} from "@lodestar/reqresp"; +import {IReqRespBeaconNode} from "./reqresp/index.js"; import {INetworkEventBus} from "./events.js"; import {Eth2Gossipsub} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; @@ -16,7 +16,7 @@ export type PeerSearchOptions = { export interface INetwork { events: INetworkEventBus; - reqResp: IReqResp; + reqResp: IReqRespBeaconNode; attnetsService: IAttnetsService; syncnetsService: ISubnetsService; gossip: Eth2Gossipsub; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 56e6617211e1..c08b77ad13e6 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -9,7 +9,6 @@ import {ATTESTATION_SUBNET_COUNT, ForkName, SYNC_COMMITTEE_SUBNET_COUNT} from "@ import {Discv5, ENR} from "@chainsafe/discv5"; import {computeEpochAtSlot, computeTimeAtSlot} from "@lodestar/state-transition"; import {altair, Epoch} from "@lodestar/types"; -import {IReqResp, ReqRespOptions} from "@lodestar/reqresp"; import {IMetrics} from "../metrics/index.js"; import {ChainEvent, IBeaconChain, IBeaconClock} from "../chain/index.js"; import {INetworkOptions} from "./options.js"; @@ -23,8 +22,7 @@ import {INetworkEventBus, NetworkEventBus} from "./events.js"; import {AttnetsService, CommitteeSubscription, SyncnetsService} from "./subnets/index.js"; import {PeersData} from "./peers/peersData.js"; import {getConnectionsMap, isPublishToZeroPeersError} from "./util.js"; -import {getBeaconNodeReqResp} from "./reqresp/index.js"; -import {ReqRespHandlers} from "./reqresp/handlers/index.js"; +import {IReqRespBeaconNode, ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/index.js"; interface INetworkModules { config: IBeaconConfig; @@ -40,7 +38,7 @@ interface INetworkModules { export class Network implements INetwork { events: INetworkEventBus; - reqResp: IReqResp; + reqResp: IReqRespBeaconNode; attnetsService: AttnetsService; syncnetsService: SyncnetsService; gossip: Eth2Gossipsub; @@ -58,8 +56,8 @@ export class Network implements INetwork { private subscribedForks = new Set(); - constructor(private readonly opts: INetworkOptions & Partial, modules: INetworkModules) { - const {config, libp2p, logger, metrics, chain, gossipHandlers, signal} = modules; + constructor(private readonly opts: INetworkOptions, modules: INetworkModules) { + const {config, libp2p, logger, metrics, chain, reqRespHandlers, gossipHandlers, signal} = modules; this.libp2p = libp2p; this.logger = logger; this.config = config; @@ -73,18 +71,19 @@ export class Network implements INetwork { this.events = networkEventBus; this.metadata = metadata; this.peerRpcScores = peerRpcScores; - this.reqResp = getBeaconNodeReqResp( + this.reqResp = new ReqRespBeaconNode( { config, libp2p, - logger, - metrics, + reqRespHandlers, metadataController: metadata, peerRpcScores, - peersData: this.peersData, + logger, networkEventBus, + metrics, + peersData: this.peersData, }, - modules.reqRespHandlers + opts ); this.gossip = new Eth2Gossipsub(opts, { diff --git a/packages/beacon-node/src/network/options.ts b/packages/beacon-node/src/network/options.ts index 837fb51a6332..64daf63730be 100644 --- a/packages/beacon-node/src/network/options.ts +++ b/packages/beacon-node/src/network/options.ts @@ -1,10 +1,10 @@ import {ENR, IDiscv5DiscoveryInputOptions} from "@chainsafe/discv5"; -import {InboundRateLimiter, RateLimiterOptions} from "@lodestar/reqresp/rate_limiter"; import {Eth2GossipsubOpts} from "./gossip/gossipsub.js"; import {defaultGossipHandlerOpts, GossipHandlerOpts} from "./gossip/handlers/index.js"; import {PeerManagerOpts} from "./peers/index.js"; +import {ReqRespBeaconNodeOpts} from "./reqresp/index.js"; -export interface INetworkOptions extends PeerManagerOpts, RateLimiterOptions, GossipHandlerOpts, Eth2GossipsubOpts { +export interface INetworkOptions extends PeerManagerOpts, ReqRespBeaconNodeOpts, GossipHandlerOpts, Eth2GossipsubOpts { localMultiaddrs: string[]; bootMultiaddrs?: string[]; subscribeAllSubnets?: boolean; @@ -27,6 +27,5 @@ export const defaultNetworkOptions: INetworkOptions = { localMultiaddrs: ["/ip4/0.0.0.0/tcp/9000"], bootMultiaddrs: [], discv5: defaultDiscv5Options, - ...InboundRateLimiter.defaults, ...defaultGossipHandlerOpts, }; diff --git a/packages/beacon-node/src/network/peers/peerManager.ts b/packages/beacon-node/src/network/peers/peerManager.ts index e31924bfb249..adea2377baf6 100644 --- a/packages/beacon-node/src/network/peers/peerManager.ts +++ b/packages/beacon-node/src/network/peers/peerManager.ts @@ -7,11 +7,11 @@ import {SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params"; import {IBeaconConfig} from "@lodestar/config"; import {allForks, altair, phase0} from "@lodestar/types"; import {ILogger} from "@lodestar/utils"; -import {IReqResp, RequestTypedContainer, ReqRespMethod} from "@lodestar/reqresp"; import {IBeaconChain} from "../../chain/index.js"; import {GoodByeReasonCode, GOODBYE_KNOWN_CODES, Libp2pEvent} from "../../constants/index.js"; import {IMetrics} from "../../metrics/index.js"; import {NetworkEvent, INetworkEventBus} from "../events.js"; +import {IReqRespBeaconNode, ReqRespMethod, RequestTypedContainer} from "../reqresp/index.js"; import {getConnection, getConnectionsMap, prettyPrintPeerId} from "../util.js"; import {ISubnetsService} from "../subnets/index.js"; import {SubnetType} from "../metadata.js"; @@ -76,7 +76,7 @@ export type PeerManagerModules = { libp2p: Libp2p; logger: ILogger; metrics: IMetrics | null; - reqResp: IReqResp; + reqResp: IReqRespBeaconNode; gossip: Eth2Gossipsub; attnetsService: ISubnetsService; syncnetsService: ISubnetsService; @@ -107,7 +107,7 @@ export class PeerManager { private libp2p: Libp2p; private logger: ILogger; private metrics: IMetrics | null; - private reqResp: IReqResp; + private reqResp: IReqRespBeaconNode; private gossipsub: Eth2Gossipsub; private attnetsService: ISubnetsService; private syncnetsService: ISubnetsService; diff --git a/packages/beacon-node/src/network/peers/peersData.ts b/packages/beacon-node/src/network/peers/peersData.ts index 3f57c5115f12..1a8fbb577295 100644 --- a/packages/beacon-node/src/network/peers/peersData.ts +++ b/packages/beacon-node/src/network/peers/peersData.ts @@ -1,6 +1,6 @@ import {PeerId} from "@libp2p/interface-peer-id"; -import {Encoding} from "@lodestar/reqresp"; import {altair} from "@lodestar/types"; +import {Encoding} from "@lodestar/reqresp"; import {ClientKind} from "./client.js"; type PeerIdStr = string; diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index dea2c3046495..d1c6aa9f2efb 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -1,4 +1,4 @@ -import {HandlerTypeFromMessage} from "@lodestar/reqresp"; +import {EncodedPayloadType, HandlerTypeFromMessage} from "@lodestar/reqresp"; import messages from "@lodestar/reqresp/messages"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; @@ -8,20 +8,8 @@ import {onLightClientBootstrap} from "./lightClientBootstrap.js"; import {onLightClientFinalityUpdate} from "./lightClientFinalityUpdate.js"; import {onLightClientOptimisticUpdate} from "./lightClientOptimisticUpdate.js"; import {onLightClientUpdatesByRange} from "./lightClientUpdatesByRange.js"; -import {onStatus} from "./onStatus.js"; -export type ReqRespHandlers = ReturnType; -/** - * The ReqRespHandler module handles app-level requests / responses from other peers, - * fetching state from the chain and database as needed. - */ -export function getReqRespHandlers({ - db, - chain, -}: { - db: IBeaconDb; - chain: IBeaconChain; -}): { +export interface ReqRespHandlers { onStatus: HandlerTypeFromMessage; onBeaconBlocksByRange: HandlerTypeFromMessage; onBeaconBlocksByRoot: HandlerTypeFromMessage; @@ -29,10 +17,15 @@ export function getReqRespHandlers({ onLightClientUpdatesByRange: HandlerTypeFromMessage; onLightClientFinalityUpdate: HandlerTypeFromMessage; onLightClientOptimisticUpdate: HandlerTypeFromMessage; -} { +} +/** + * The ReqRespHandler module handles app-level requests / responses from other peers, + * fetching state from the chain and database as needed. + */ +export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconChain}): ReqRespHandlers { return { async *onStatus() { - yield* onStatus(chain); + yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; }, async *onBeaconBlocksByRange(req) { yield* onBeaconBlocksByRange(req, chain, db); diff --git a/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts b/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts deleted file mode 100644 index e05f6166c3b0..000000000000 --- a/packages/beacon-node/src/network/reqresp/handlers/onStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {EncodedPayload, EncodedPayloadType} from "@lodestar/reqresp"; -import {phase0} from "@lodestar/types"; -import {IBeaconChain} from "../../../chain/index.js"; - -export async function* onStatus(chain: IBeaconChain): AsyncIterable> { - yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; -} diff --git a/packages/reqresp/src/rate_limiter/RateLimiter.ts b/packages/beacon-node/src/network/reqresp/inboundRateLimiter.ts similarity index 78% rename from packages/reqresp/src/rate_limiter/RateLimiter.ts rename to packages/beacon-node/src/network/reqresp/inboundRateLimiter.ts index 54cc10a32f74..720c6dd8c83e 100644 --- a/packages/reqresp/src/rate_limiter/RateLimiter.ts +++ b/packages/beacon-node/src/network/reqresp/inboundRateLimiter.ts @@ -1,13 +1,12 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ILogger, MapDef} from "@lodestar/utils"; -import {RateLimiter} from "../interface.js"; -import {Metrics} from "../metrics.js"; -import {RateTracker} from "./RateTracker.js"; +import {IMetrics} from "../../metrics/index.js"; +import {RateTracker} from "./rateTracker.js"; interface RateLimiterModules { logger: ILogger; reportPeer: (peer: PeerId) => void; - metrics: Metrics | null; + metrics: IMetrics | null; } /** @@ -18,10 +17,17 @@ interface RateLimiterModules { * - rateTrackerTimeoutMs: the time period we want to track total requests or objects, normally 1 min */ export type RateLimiterOptions = { - requestCountPeerLimit: number; - blockCountPeerLimit: number; - blockCountTotalLimit: number; - rateTrackerTimeoutMs: number; + requestCountPeerLimit?: number; + blockCountPeerLimit?: number; + blockCountTotalLimit?: number; + rateTrackerTimeoutMs?: number; +}; + +export const defaultRateLimiterOpts: Required = { + requestCountPeerLimit: 50, + blockCountPeerLimit: 500, + blockCountTotalLimit: 2000, + rateTrackerTimeoutMs: 60 * 1000, }; /** Sometimes a peer request comes AFTER libp2p disconnect event, check for such peers every 10 minutes */ @@ -34,10 +40,10 @@ const DISCONNECTED_TIMEOUT_MS = 5 * 60 * 1000; * This class is singleton, it has per-peer request count rate tracker and block count rate tracker * and a block count rate tracker for all peers (this is lodestar specific). */ -export class InboundRateLimiter implements RateLimiter { +export class InboundRateLimiter { private readonly logger: ILogger; private readonly reportPeer: RateLimiterModules["reportPeer"]; - private readonly metrics: Metrics | null; + private readonly metrics: IMetrics | null; private requestCountTrackersByPeer: MapDef; /** * This rate tracker is specific to lodestar, we don't want to serve too many blocks for peers at the @@ -49,24 +55,10 @@ export class InboundRateLimiter implements RateLimiter { private lastSeenRequestsByPeer: Map; /** Interval to check lastSeenMessagesByPeer */ private cleanupInterval: NodeJS.Timeout | undefined = undefined; - private options: RateLimiterOptions; + private options: Required; - /** - * Default value for RateLimiterOpts - * - requestCountPeerLimit: allow to serve 50 requests per peer within 1 minute - * - blockCountPeerLimit: allow to serve 500 blocks per peer within 1 minute - * - blockCountTotalLimit: allow to serve 2000 (blocks) for all peer within 1 minute (4 x blockCountPeerLimit) - * - rateTrackerTimeoutMs: 1 minute - */ - static defaults: RateLimiterOptions = { - requestCountPeerLimit: 50, - blockCountPeerLimit: 500, - blockCountTotalLimit: 2000, - rateTrackerTimeoutMs: 60 * 1000, - }; - - constructor(options: Partial, modules: RateLimiterModules) { - this.options = {...InboundRateLimiter.defaults, ...options}; + constructor(options: RateLimiterOptions, modules: RateLimiterModules) { + this.options = {...defaultRateLimiterOpts, ...options}; this.reportPeer = modules.reportPeer; this.requestCountTrackersByPeer = new MapDef( @@ -110,7 +102,7 @@ export class InboundRateLimiter implements RateLimiter { }); this.reportPeer(peerId); if (this.metrics) { - this.metrics.rateLimitErrors.inc({tracker: "requestCountPeerTracker"}); + this.metrics.reqResp.rateLimitErrors.inc({tracker: "requestCountPeerTracker"}); } return false; } @@ -133,14 +125,14 @@ export class InboundRateLimiter implements RateLimiter { }); this.reportPeer(peerId); if (this.metrics) { - this.metrics.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); + this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountPeerTracker"}); } return false; } if (this.blockCountTotalTracker.requestObjects(numBlock) === 0) { if (this.metrics) { - this.metrics.rateLimitErrors.inc({tracker: "blockCountTotalTracker"}); + this.metrics.reqResp.rateLimitErrors.inc({tracker: "blockCountTotalTracker"}); } // don't apply penalty return false; diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index ef1b8ff57f17..cff2dd8cc273 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -11,6 +11,8 @@ import { collectMaxResponse, EncodedPayload, EncodedPayloadType, + Encoding, + ProtocolDefinition, ReqResp, RequestError, ResponseError, @@ -18,14 +20,19 @@ import { import messages from "@lodestar/reqresp/messages"; import {IMetrics} from "../../metrics/metrics.js"; import {INetworkEventBus, NetworkEvent} from "../events.js"; -import {IPeerRpcScoreStore} from "../peers/score.js"; +import {IPeerRpcScoreStore, PeerAction} from "../peers/score.js"; import {MetadataController} from "../metadata.js"; -import {PeerData, PeersData} from "../peers/peersData.js"; +import {PeersData} from "../peers/peersData.js"; import {ReqRespHandlers} from "./handlers/index.js"; import {collectSequentialBlocksInRange} from "./utils/collectSequentialBlocksInRange.js"; -import {IReqResp, RateLimiter, RespStatus} from "./interface.js"; -import {Method, RequestTypedContainer, Version} from "./types.js"; +import {IReqRespBeaconNode, RespStatus} from "./interface.js"; +import {ReqRespMethod, RequestTypedContainer, Version} from "./types.js"; import {onOutgoingReqRespError} from "./score.js"; +import {InboundRateLimiter, RateLimiterOptions} from "./inboundRateLimiter.js"; + +export {IReqRespBeaconNode}; +export {ReqRespMethod, RequestTypedContainer} from "./types.js"; +export {getReqRespHandlers, ReqRespHandlers} from "./handlers/index.js"; /** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ export type ReqRespBlockResponse = { @@ -46,29 +53,49 @@ export interface ReqRespBeaconNodeModules { networkEventBus: INetworkEventBus; } +export interface ReqRespBeaconNodeOpts extends ReqRespOpts, RateLimiterOptions { + /** maximum request count we can serve per peer within rateTrackerTimeoutMs */ + requestCountPeerLimit?: number; + /** maximum block count we can serve per peer within rateTrackerTimeoutMs */ + blockCountPeerLimit?: number; + /** maximum block count we can serve for all peers within rateTrackerTimeoutMs */ + blockCountTotalLimit?: number; + /** the time period we want to track total requests or objects, normally 1 min */ + rateTrackerTimeoutMs?: number; +} + /** * Implementation of Ethereum Consensus p2p Req/Resp domain. * For the spec that this code is based on, see: * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ -export class ReqRespBeaconNode extends ReqResp implements IReqResp { +export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { private readonly reqRespHandlers: ReqRespHandlers; private readonly metadataController: MetadataController; private readonly peerRpcScores: IPeerRpcScoreStore; - private readonly inboundRateLimiter: RateLimiter; + private readonly inboundRateLimiter: InboundRateLimiter; private readonly networkEventBus: INetworkEventBus; - private readonly peerData: PeerData; + private readonly peersData: PeersData; + + constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) { + const {reqRespHandlers, networkEventBus, peersData, peerRpcScores, metadataController, logger, metrics} = modules; + + super({...modules, metrics: metrics?.reqResp ?? null}, options); - constructor(modules: ReqRespBeaconNodeModules, options?: Partial & Partial) { - super(modules, {...defaultReqRespOptions, ...options}); - const reqRespHandlers: ReqRespHandlers = 1; - this.peerRpcScores = modules.peerRpcScores; - this.metadataController = modules.metadataController; - this.inboundRateLimiter = new InboundRateLimiter({...InboundRateLimiter.defaults, ...options}, modules); + this.reqRespHandlers = reqRespHandlers; + this.peerRpcScores = peerRpcScores; + this.peersData = peersData; + this.metadataController = metadataController; + this.networkEventBus = networkEventBus; + this.inboundRateLimiter = new InboundRateLimiter(options, { + logger, + reportPeer: (peerId) => peerRpcScores.applyAction(peerId, PeerAction.Fatal, "rate_limit_rpc"), + metrics, + }); // TODO: Do not register everything! Some protocols are fork dependant - this.registerProtocol(messages.Ping(modules, this.onPing.bind(this))); + this.registerProtocol(messages.Ping(this.onPing.bind(this))); this.registerProtocol(messages.Status(modules, this.onStatus.bind(this))); this.registerProtocol(messages.Metadata(modules, this.onMetadata.bind(this))); this.registerProtocol(messages.MetadataV2(modules, this.onMetadata.bind(this))); @@ -99,27 +126,32 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { async status(peerId: PeerId, request: phase0.Status): Promise { return collectExactOne( - this.sendRequest(peerId, Method.Status, [Version.V1], request) + this.sendRequest(peerId, ReqRespMethod.Status, [Version.V1], request) ); } async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { // TODO: Replace with "ignore response after request" await collectExactOne( - this.sendRequest(peerId, Method.Goodbye, [Version.V1], request) + this.sendRequest(peerId, ReqRespMethod.Goodbye, [Version.V1], request) ); } async ping(peerId: PeerId): Promise { return collectExactOne( - this.sendRequest(peerId, Method.Ping, [Version.V1], this.metadataController.seqNumber) + this.sendRequest( + peerId, + ReqRespMethod.Ping, + [Version.V1], + this.metadataController.seqNumber + ) ); } async metadata(peerId: PeerId, fork?: ForkName): Promise { // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; - return collectExactOne(this.sendRequest(peerId, Method.Metadata, versions, null)); + return collectExactOne(this.sendRequest(peerId, ReqRespMethod.Metadata, versions, null)); } async beaconBlocksByRange( @@ -129,7 +161,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { return collectSequentialBlocksInRange( this.sendRequest( peerId, - Method.BeaconBlocksByRange, + ReqRespMethod.BeaconBlocksByRange, [Version.V2, Version.V1], // Prioritize V2 request ), @@ -144,7 +176,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { return collectMaxResponse( this.sendRequest( peerId, - Method.BeaconBlocksByRoot, + ReqRespMethod.BeaconBlocksByRoot, [Version.V2, Version.V1], // Prioritize V2 request ), @@ -154,7 +186,12 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { async lightClientBootstrap(peerId: PeerId, request: Root): Promise { return collectExactOne( - this.sendRequest(peerId, Method.LightClientBootstrap, [Version.V1], request) + this.sendRequest( + peerId, + ReqRespMethod.LightClientBootstrap, + [Version.V1], + request + ) ); } @@ -162,7 +199,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { return collectExactOne( this.sendRequest( peerId, - Method.LightClientOptimisticUpdate, + ReqRespMethod.LightClientOptimisticUpdate, [Version.V1], null ) @@ -173,7 +210,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { return collectExactOne( this.sendRequest( peerId, - Method.LightClientFinalityUpdate, + ReqRespMethod.LightClientFinalityUpdate, [Version.V1], null ) @@ -187,7 +224,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { return collectMaxResponse( this.sendRequest( peerId, - Method.LightClientUpdatesByRange, + ReqRespMethod.LightClientUpdatesByRange, [Version.V1], request ), @@ -209,13 +246,18 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); } - protected onIncomingRequest(peerId: PeerId, method: Method): void { - if (method !== Method.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { + protected onIncomingRequest(peerId: PeerId, protocol: ProtocolDefinition): void { + if (protocol.method !== ReqRespMethod.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); } + + // Remember prefered encoding + if (protocol.method === ReqRespMethod.Status) { + this.peersData.setEncodingPreference(peerId.toString(), protocol.encoding); + } } - protected onOutgoingRequestError(peerId: PeerId, method: Method, error: RequestError): void { + protected onOutgoingRequestError(peerId: PeerId, method: ReqRespMethod, error: RequestError): void { const peerAction = onOutgoingReqRespError(error, method); if (peerAction !== null) { this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); @@ -223,24 +265,23 @@ export class ReqRespBeaconNode extends ReqResp implements IReqResp { } private async *onStatus(req: phase0.Status, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Status, body: req}, peerId); - // Remember prefered encoding - const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; + this.onIncomingRequestBody({method: ReqRespMethod.Status, body: req}, peerId); + yield* this.reqRespHandlers.onStatus(req, peerId); } private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); + this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body: req}, peerId); yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; } private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Goodbye, body: req}, peerId); + this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body: req}, peerId); yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; } private async *onMetadata(req: null, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: Method.Metadata, body: req}, peerId); + this.onIncomingRequestBody({method: ReqRespMethod.Metadata, body: req}, peerId); // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property // It's safe to return altair.Metadata here for all versions diff --git a/packages/beacon-node/src/network/reqresp/interface.ts b/packages/beacon-node/src/network/reqresp/interface.ts index 641b7d4504fc..a55c3bbf6c02 100644 --- a/packages/beacon-node/src/network/reqresp/interface.ts +++ b/packages/beacon-node/src/network/reqresp/interface.ts @@ -2,7 +2,7 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {ForkName} from "@lodestar/params"; import {allForks, altair, phase0} from "@lodestar/types"; -export interface IReqResp { +export interface IReqRespBeaconNode { start(): void; stop(): void; status(peerId: PeerId, request: phase0.Status): Promise; diff --git a/packages/reqresp/src/rate_limiter/RateTracker.ts b/packages/beacon-node/src/network/reqresp/rateTracker.ts similarity index 100% rename from packages/reqresp/src/rate_limiter/RateTracker.ts rename to packages/beacon-node/src/network/reqresp/rateTracker.ts diff --git a/packages/beacon-node/src/network/reqresp/score.ts b/packages/beacon-node/src/network/reqresp/score.ts index 157e5d979ec6..f77e9273b704 100644 --- a/packages/beacon-node/src/network/reqresp/score.ts +++ b/packages/beacon-node/src/network/reqresp/score.ts @@ -1,6 +1,6 @@ import {RequestError, RequestErrorCode} from "@lodestar/reqresp"; import {PeerAction} from "../peers/score.js"; -import {Method} from "./types.js"; +import {ReqRespMethod} from "./types.js"; /** * libp2p-ts does not include types for the error codes. @@ -20,7 +20,7 @@ const multiStreamSelectErrorCodes = { protocolSelectionFailed: "protocol selection failed", }; -export function onOutgoingReqRespError(e: RequestError, method: Method): PeerAction | null { +export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod): PeerAction | null { switch (e.type.code) { case RequestErrorCode.INVALID_REQUEST: return PeerAction.LowToleranceError; @@ -32,7 +32,7 @@ export function onOutgoingReqRespError(e: RequestError, method: Method): PeerAct case RequestErrorCode.DIAL_TIMEOUT: case RequestErrorCode.DIAL_ERROR: - return e.message.includes(multiStreamSelectErrorCodes.protocolSelectionFailed) && method === Method.Ping + return e.message.includes(multiStreamSelectErrorCodes.protocolSelectionFailed) && method === ReqRespMethod.Ping ? PeerAction.Fatal : PeerAction.LowToleranceError; // TODO: Detect SSZDecodeError and return PeerAction.Fatal @@ -40,10 +40,10 @@ export function onOutgoingReqRespError(e: RequestError, method: Method): PeerAct case RequestErrorCode.TTFB_TIMEOUT: case RequestErrorCode.RESP_TIMEOUT: switch (method) { - case Method.Ping: + case ReqRespMethod.Ping: return PeerAction.LowToleranceError; - case Method.BeaconBlocksByRange: - case Method.BeaconBlocksByRoot: + case ReqRespMethod.BeaconBlocksByRange: + case ReqRespMethod.BeaconBlocksByRoot: return PeerAction.MidToleranceError; default: return null; @@ -52,10 +52,10 @@ export function onOutgoingReqRespError(e: RequestError, method: Method): PeerAct if (e.message.includes(libp2pErrorCodes.ERR_UNSUPPORTED_PROTOCOL)) { switch (method) { - case Method.Ping: + case ReqRespMethod.Ping: return PeerAction.Fatal; - case Method.Metadata: - case Method.Status: + case ReqRespMethod.Metadata: + case ReqRespMethod.Status: return PeerAction.LowToleranceError; default: return null; diff --git a/packages/beacon-node/src/network/reqresp/types.ts b/packages/beacon-node/src/network/reqresp/types.ts index c23f5c3b9717..048a417f0d43 100644 --- a/packages/beacon-node/src/network/reqresp/types.ts +++ b/packages/beacon-node/src/network/reqresp/types.ts @@ -1,7 +1,7 @@ import {phase0} from "@lodestar/types"; /** ReqResp protocol names or methods. Each Method can have multiple versions and encodings */ -export enum Method { +export enum ReqRespMethod { // Phase 0 Status = "status", Goodbye = "goodbye", @@ -17,22 +17,22 @@ export enum Method { // To typesafe events to network type RequestBodyByMethod = { - [Method.Status]: phase0.Status; - [Method.Goodbye]: phase0.Goodbye; - [Method.Ping]: phase0.Ping; - [Method.Metadata]: null; + [ReqRespMethod.Status]: phase0.Status; + [ReqRespMethod.Goodbye]: phase0.Goodbye; + [ReqRespMethod.Ping]: phase0.Ping; + [ReqRespMethod.Metadata]: null; // Do not matter - [Method.BeaconBlocksByRange]: unknown; - [Method.BeaconBlocksByRoot]: unknown; - [Method.LightClientBootstrap]: unknown; - [Method.LightClientUpdatesByRange]: unknown; - [Method.LightClientFinalityUpdate]: unknown; - [Method.LightClientOptimisticUpdate]: unknown; + [ReqRespMethod.BeaconBlocksByRange]: unknown; + [ReqRespMethod.BeaconBlocksByRoot]: unknown; + [ReqRespMethod.LightClientBootstrap]: unknown; + [ReqRespMethod.LightClientUpdatesByRange]: unknown; + [ReqRespMethod.LightClientFinalityUpdate]: unknown; + [ReqRespMethod.LightClientOptimisticUpdate]: unknown; }; export type RequestTypedContainer = { - [K in Method]: {method: K; body: RequestBodyByMethod[K]}; -}[Method]; + [K in ReqRespMethod]: {method: K; body: RequestBodyByMethod[K]}; +}[ReqRespMethod]; export enum Version { V1 = 1, diff --git a/packages/beacon-node/src/node/nodejs.ts b/packages/beacon-node/src/node/nodejs.ts index 8aa503e5b41f..0647225302ab 100644 --- a/packages/beacon-node/src/node/nodejs.ts +++ b/packages/beacon-node/src/node/nodejs.ts @@ -10,7 +10,7 @@ import {BeaconStateAllForks} from "@lodestar/state-transition"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {IBeaconDb} from "../db/index.js"; -import {INetwork, Network} from "../network/index.js"; +import {INetwork, Network, getReqRespHandlers} from "../network/index.js"; import {BeaconSync, IBeaconSync} from "../sync/index.js"; import {BackfillSync} from "../sync/backfill/index.js"; import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js"; @@ -19,7 +19,6 @@ import {getApi, BeaconRestApiServer} from "../api/index.js"; import {initializeExecutionEngine, initializeExecutionBuilder} from "../execution/index.js"; import {initializeEth1ForBlockProduction} from "../eth1/index.js"; import {createLibp2pMetrics} from "../metrics/metrics/libp2p.js"; -import {getReqRespHandlers} from "../network/reqresp/handlers/index.js"; import {IBeaconNodeOptions} from "./options.js"; import {runNodeNotifier} from "./notifier.js"; diff --git a/packages/beacon-node/test/e2e/network/reqresp.test.ts b/packages/beacon-node/test/e2e/network/reqresp.test.ts index efd27148f77d..2151184aa668 100644 --- a/packages/beacon-node/test/e2e/network/reqresp.test.ts +++ b/packages/beacon-node/test/e2e/network/reqresp.test.ts @@ -9,7 +9,7 @@ import {ForkName} from "@lodestar/params"; import {BitArray} from "@chainsafe/ssz"; import {IReqRespOptions, Network} from "../../../src/network/index.js"; import {defaultNetworkOptions, INetworkOptions} from "../../../src/network/options.js"; -import {Method, Encoding} from "../../../src/network/reqresp/types.js"; +import {ReqRespMethod, Encoding} from "../../../src/network/reqresp/types.js"; import {ReqRespHandlers} from "../../../src/network/reqresp/handlers/index.js"; import {RequestError, RequestErrorCode} from "../../../src/network/reqresp/request/index.js"; import {IRequestErrorMetadata} from "../../../src/network/reqresp/request/errors.js"; @@ -280,7 +280,7 @@ describe("network / ReqResp", function () { netA.reqResp.beaconBlocksByRange(netB.peerId, {startSlot: 0, step: 1, count: 3}), new RequestError( {code: RequestErrorCode.SERVER_ERROR, errorMessage: "sNaPpYa" + testErrorMessage}, - formatMetadata(Method.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) + formatMetadata(ReqRespMethod.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) ) ); }); @@ -299,7 +299,7 @@ describe("network / ReqResp", function () { netA.reqResp.beaconBlocksByRange(netB.peerId, {startSlot: 0, step: 1, count: 3}), new RequestError( {code: RequestErrorCode.SERVER_ERROR, errorMessage: "sNaPpYa" + testErrorMessage}, - formatMetadata(Method.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) + formatMetadata(ReqRespMethod.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) ) ); }); @@ -322,7 +322,7 @@ describe("network / ReqResp", function () { netA.reqResp.beaconBlocksByRange(netB.peerId, {startSlot: 0, step: 1, count: 1}), new RequestError( {code: RequestErrorCode.TTFB_TIMEOUT}, - formatMetadata(Method.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) + formatMetadata(ReqRespMethod.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) ) ); }); @@ -346,7 +346,7 @@ describe("network / ReqResp", function () { netA.reqResp.beaconBlocksByRange(netB.peerId, {startSlot: 0, step: 1, count: 2}), new RequestError( {code: RequestErrorCode.RESP_TIMEOUT}, - formatMetadata(Method.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) + formatMetadata(ReqRespMethod.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) ) ); }); @@ -365,7 +365,7 @@ describe("network / ReqResp", function () { netA.reqResp.beaconBlocksByRange(netB.peerId, {startSlot: 0, step: 1, count: 2}), new RequestError( {code: RequestErrorCode.TTFB_TIMEOUT}, - formatMetadata(Method.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) + formatMetadata(ReqRespMethod.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) ) ); }); @@ -385,13 +385,13 @@ describe("network / ReqResp", function () { netA.reqResp.beaconBlocksByRange(netB.peerId, {startSlot: 0, step: 1, count: 2}), new RequestError( {code: RequestErrorCode.RESP_TIMEOUT}, - formatMetadata(Method.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) + formatMetadata(ReqRespMethod.BeaconBlocksByRange, Encoding.SSZ_SNAPPY, netB.peerId) ) ); }); }); /** Helper to reduce code-duplication */ -function formatMetadata(method: Method, encoding: Encoding, peer: PeerId): IRequestErrorMetadata { +function formatMetadata(method: ReqRespMethod, encoding: Encoding, peer: PeerId): IRequestErrorMetadata { return {method, encoding, peer: peer.toString()}; } diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts index fd37428b98d0..d949423e9c8b 100644 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts @@ -2,7 +2,7 @@ import {expect} from "chai"; import all from "it-all"; import {pipe} from "it-pipe"; import {Uint8ArrayList} from "uint8arraylist"; -import {Method, Encoding, RequestBody} from "../../../../../src/network/reqresp/types.js"; +import {ReqRespMethod, Encoding, RequestBody} from "../../../../../src/network/reqresp/types.js"; import {SszSnappyErrorCode} from "../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; import {requestEncode} from "../../../../../src/network/reqresp/encoders/requestEncode.js"; import {requestDecode} from "../../../../../src/network/reqresp/encoders/requestDecode.js"; @@ -12,7 +12,7 @@ import {arrToSource, expectEqualByteChunks} from "../utils.js"; describe("network / reqresp / encoders / request - Success and error cases", () => { const testCases: { id: string; - method: Method; + method: ReqRespMethod; encoding: Encoding; chunks: Uint8ArrayList[]; // decode @@ -22,28 +22,28 @@ describe("network / reqresp / encoders / request - Success and error cases", () }[] = [ { id: "Bad body", - method: Method.Status, + method: ReqRespMethod.Status, encoding: Encoding.SSZ_SNAPPY, errorDecode: SszSnappyErrorCode.UNDER_SSZ_MIN_SIZE, chunks: [new Uint8ArrayList(Buffer.from("4"))], }, { id: "No body on Metadata - Ok", - method: Method.Metadata, + method: ReqRespMethod.Metadata, encoding: Encoding.SSZ_SNAPPY, requestBody: null, chunks: [], }, { id: "No body on Status - Error", - method: Method.Status, + method: ReqRespMethod.Status, encoding: Encoding.SSZ_SNAPPY, errorDecode: SszSnappyErrorCode.SOURCE_ABORTED, chunks: [], }, { id: "Regular request", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, requestBody: sszSnappyPing.body, chunks: sszSnappyPing.chunks, diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts index b4ce475bf8bb..eda4777369b2 100644 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts @@ -1,7 +1,12 @@ import {expect} from "chai"; import {pipe} from "it-pipe"; import {phase0} from "@lodestar/types"; -import {Method, Encoding, getRequestSzzTypeByMethod, RequestBody} from "../../../../../src/network/reqresp/types.js"; +import { + ReqRespMethod, + Encoding, + getRequestSzzTypeByMethod, + RequestBody, +} from "../../../../../src/network/reqresp/types.js"; import {requestEncode} from "../../../../../src/network/reqresp/encoders/requestEncode.js"; import {requestDecode} from "../../../../../src/network/reqresp/encoders/requestDecode.js"; import {isEqualSszType} from "../../../../utils/ssz.js"; @@ -10,21 +15,21 @@ import {createStatus, generateRoots} from "../utils.js"; // Ensure the types from all methods are supported properly describe("network / reqresp / encoders / request - types", () => { interface IRequestTypes { - [Method.Status]: phase0.Status; - [Method.Goodbye]: phase0.Goodbye; - [Method.Ping]: phase0.Ping; - [Method.Metadata]: null; - [Method.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest; - [Method.BeaconBlocksByRoot]: phase0.BeaconBlocksByRootRequest; + [ReqRespMethod.Status]: phase0.Status; + [ReqRespMethod.Goodbye]: phase0.Goodbye; + [ReqRespMethod.Ping]: phase0.Ping; + [ReqRespMethod.Metadata]: null; + [ReqRespMethod.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest; + [ReqRespMethod.BeaconBlocksByRoot]: phase0.BeaconBlocksByRootRequest; } const testCases: {[P in keyof IRequestTypes]: IRequestTypes[P][]} = { - [Method.Status]: [createStatus()], - [Method.Goodbye]: [BigInt(0), BigInt(1)], - [Method.Ping]: [BigInt(0), BigInt(1)], - [Method.Metadata]: [], - [Method.BeaconBlocksByRange]: [{startSlot: 10, count: 20, step: 1}], - [Method.BeaconBlocksByRoot]: [generateRoots(4, 0xda)], + [ReqRespMethod.Status]: [createStatus()], + [ReqRespMethod.Goodbye]: [BigInt(0), BigInt(1)], + [ReqRespMethod.Ping]: [BigInt(0), BigInt(1)], + [ReqRespMethod.Metadata]: [], + [ReqRespMethod.BeaconBlocksByRange]: [{startSlot: 10, count: 20, step: 1}], + [ReqRespMethod.BeaconBlocksByRoot]: [generateRoots(4, 0xda)], }; const encodings: Encoding[] = [Encoding.SSZ_SNAPPY]; diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts index d650ed02c149..303b48807c0b 100644 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts @@ -7,7 +7,7 @@ import {createIBeaconConfig} from "@lodestar/config"; import {fromHex, LodestarError} from "@lodestar/utils"; import {allForks} from "@lodestar/types"; import { - Method, + ReqRespMethod, Version, Encoding, Protocol, @@ -42,7 +42,7 @@ type ResponseChunk = | {status: Exclude; errorMessage: string}; describe("network / reqresp / encoders / response - Success and error cases", () => { - const methodDefault = Method.Status; + const methodDefault = ReqRespMethod.Status; const encodingDefault = Encoding.SSZ_SNAPPY; // Set the altair fork to happen between the two precomputed SSZ snappy blocks @@ -57,7 +57,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () const testCases: { id: string; - method?: Method; + method?: ReqRespMethod; version?: Version; encoding?: Encoding; chunks?: Uint8ArrayList[]; @@ -70,14 +70,14 @@ describe("network / reqresp / encoders / response - Success and error cases", () }[] = [ { id: "No chunks should be ok", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, responseChunks: [], chunks: [], }, { id: "Empty response chunk - Error", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, decodeError: new SszSnappyError({code: SszSnappyErrorCode.SOURCE_ABORTED}), chunks: [new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS]))], @@ -85,7 +85,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () }, { id: "Single chunk - wrong body SSZ type", - method: Method.Status, + method: ReqRespMethod.Status, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, responseChunks: [{status: RespStatus.SUCCESS, body: BigInt(1)}], @@ -100,7 +100,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () }, { id: "block v1 without ", - method: Method.BeaconBlocksByRange, + method: ReqRespMethod.BeaconBlocksByRange, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, responseChunks: [{status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockPhase0.body}], @@ -113,7 +113,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () }, { id: "block v2 with phase0", - method: Method.BeaconBlocksByRange, + method: ReqRespMethod.BeaconBlocksByRange, version: Version.V2, encoding: Encoding.SSZ_SNAPPY, responseChunks: [{status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockPhase0.body}], @@ -128,7 +128,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () }, { id: "block v2 with altair", - method: Method.BeaconBlocksByRange, + method: ReqRespMethod.BeaconBlocksByRange, version: Version.V2, encoding: Encoding.SSZ_SNAPPY, responseChunks: [{status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockAltair.body}], @@ -144,7 +144,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () { id: "Multiple chunks with success", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, responseChunks: [ {status: RespStatus.SUCCESS, body: sszSnappyPing.body}, @@ -161,7 +161,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () }, { id: "Multiple chunks with final error, should error", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, decodeError: new ResponseError(RespStatus.SERVER_ERROR, ""), responseChunks: [ @@ -182,7 +182,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () }, { id: "Decode successfully response_chunk as a single Uint8ArrayList", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, responseChunks: [ {status: RespStatus.SUCCESS, body: BigInt(1)}, @@ -199,7 +199,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () { id: "Decode successfully response_chunk as a single concated chunk", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, responseChunks: [ {status: RespStatus.SUCCESS, body: BigInt(1)}, @@ -220,7 +220,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () { id: "Decode blocks v2 through a fork with multiple types", - method: Method.BeaconBlocksByRange, + method: ReqRespMethod.BeaconBlocksByRange, version: Version.V2, encoding: Encoding.SSZ_SNAPPY, responseChunks: [ @@ -285,7 +285,7 @@ describe("network / reqresp / encoders / response - Success and error cases", () for (const chunk of responseChunks) { if (chunk.status === RespStatus.SUCCESS) { const lodestarResponseBodies = - protocol.method === Method.BeaconBlocksByRange || protocol.method === Method.BeaconBlocksByRoot + protocol.method === ReqRespMethod.BeaconBlocksByRange || protocol.method === ReqRespMethod.BeaconBlocksByRoot ? blocksToReqRespBlockResponses([chunk.body] as allForks.SignedBeaconBlock[], config) : [chunk.body]; yield* pipe( diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts index f3a5912c1534..41b0ba10beef 100644 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts @@ -3,7 +3,7 @@ import all from "it-all"; import {allForks, ssz} from "@lodestar/types"; import {ForkName} from "@lodestar/params"; import { - Method, + ReqRespMethod, Version, Encoding, OutgoingResponseBody, @@ -20,16 +20,16 @@ import {blocksToReqRespBlockResponses} from "../../../../utils/block.js"; // Ensure the types from all methods are supported properly describe("network / reqresp / encoders / responseTypes", () => { const testCases: {[P in keyof IncomingResponseBodyByMethod]: IncomingResponseBodyByMethod[P][][]} = { - [Method.Status]: [[createStatus()]], - [Method.Goodbye]: [[BigInt(0)], [BigInt(1)]], - [Method.Ping]: [[BigInt(0)], [BigInt(1)]], - [Method.Metadata]: [], - [Method.BeaconBlocksByRange]: [generateEmptySignedBlocks(2)], - [Method.BeaconBlocksByRoot]: [generateEmptySignedBlocks(2)], - [Method.LightClientBootstrap]: [[ssz.altair.LightClientBootstrap.defaultValue()]], - [Method.LightClientUpdatesByRange]: [[ssz.altair.LightClientUpdate.defaultValue()]], - [Method.LightClientFinalityUpdate]: [[ssz.altair.LightClientFinalityUpdate.defaultValue()]], - [Method.LightClientOptimisticUpdate]: [[ssz.altair.LightClientOptimisticUpdate.defaultValue()]], + [ReqRespMethod.Status]: [[createStatus()]], + [ReqRespMethod.Goodbye]: [[BigInt(0)], [BigInt(1)]], + [ReqRespMethod.Ping]: [[BigInt(0)], [BigInt(1)]], + [ReqRespMethod.Metadata]: [], + [ReqRespMethod.BeaconBlocksByRange]: [generateEmptySignedBlocks(2)], + [ReqRespMethod.BeaconBlocksByRoot]: [generateEmptySignedBlocks(2)], + [ReqRespMethod.LightClientBootstrap]: [[ssz.altair.LightClientBootstrap.defaultValue()]], + [ReqRespMethod.LightClientUpdatesByRange]: [[ssz.altair.LightClientUpdate.defaultValue()]], + [ReqRespMethod.LightClientFinalityUpdate]: [[ssz.altair.LightClientFinalityUpdate.defaultValue()]], + [ReqRespMethod.LightClientOptimisticUpdate]: [[ssz.altair.LightClientOptimisticUpdate.defaultValue()]], }; const encodings: Encoding[] = [Encoding.SSZ_SNAPPY]; @@ -43,12 +43,12 @@ describe("network / reqresp / encoders / responseTypes", () => { const method = _method as keyof typeof testCases; // const responsesChunks = _responsesChunks as LodestarResponseBody[][]; const lodestarResponseBodies = - _method === Method.BeaconBlocksByRange || _method === Method.BeaconBlocksByRoot + _method === ReqRespMethod.BeaconBlocksByRange || _method === ReqRespMethod.BeaconBlocksByRoot ? responsesChunks.map((chunk) => blocksToReqRespBlockResponses(chunk as allForks.SignedBeaconBlock[])) : (responsesChunks as OutgoingResponseBody[][]); const versions = - method === Method.BeaconBlocksByRange || method === Method.BeaconBlocksByRoot + method === ReqRespMethod.BeaconBlocksByRange || method === ReqRespMethod.BeaconBlocksByRoot ? [Version.V1, Version.V2] : [Version.V1]; diff --git a/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts b/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts index 2975c2b94636..f2203e58cd2d 100644 --- a/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts @@ -1,6 +1,6 @@ import {expect} from "chai"; import {collectResponses} from "../../../../../src/network/reqresp/request/collectResponses.js"; -import {Method, IncomingResponseBody} from "../../../../../src/network/reqresp/types.js"; +import {ReqRespMethod, IncomingResponseBody} from "../../../../../src/network/reqresp/types.js"; import {arrToSource} from "../utils.js"; describe("network / reqresp / request / collectResponses", () => { @@ -8,20 +8,20 @@ describe("network / reqresp / request / collectResponses", () => { const testCases: { id: string; - method: Method; + method: ReqRespMethod; maxResponses?: number; sourceChunks: IncomingResponseBody[]; expectedReturn: IncomingResponseBody | IncomingResponseBody[]; }[] = [ { id: "Return first chunk only for a single-chunk method", - method: Method.Ping, + method: ReqRespMethod.Ping, sourceChunks: [chunk, chunk], expectedReturn: chunk, }, { id: "Return up to maxResponses for a multi-chunk method", - method: Method.BeaconBlocksByRange, + method: ReqRespMethod.BeaconBlocksByRange, sourceChunks: [chunk, chunk, chunk], maxResponses: 2, expectedReturn: [chunk, chunk], diff --git a/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts b/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts index 891428096e27..a0c4d2740200 100644 --- a/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts @@ -12,7 +12,7 @@ import { RequestErrorCode, } from "../../../../../src/network/reqresp/request/errors.js"; import {sendRequest} from "../../../../../src/network/reqresp/request/index.js"; -import {Encoding, Method, Version} from "../../../../../src/network/reqresp/types.js"; +import {Encoding, ReqRespMethod, Version} from "../../../../../src/network/reqresp/types.js"; import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; import {getValidPeerId} from "../../../../utils/peer.js"; import {testLogger} from "../../../../utils/logger.js"; @@ -32,7 +32,7 @@ describe("network / reqresp / request / responseTimeoutsHandler", () => { } // Generic request params not relevant to timeout tests - const method = Method.BeaconBlocksByRange; + const method = ReqRespMethod.BeaconBlocksByRange; const encoding = Encoding.SSZ_SNAPPY; const version = Version.V1; const requestBody: phase0.BeaconBlocksByRangeRequest = {startSlot: 0, count: 9, step: 1}; diff --git a/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts b/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts index d24adf84f3b6..afea9fdec567 100644 --- a/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts @@ -2,7 +2,7 @@ import {expect} from "chai"; import {Uint8ArrayList} from "uint8arraylist"; import {LodestarError, fromHex} from "@lodestar/utils"; import {RespStatus} from "../../../../../src/constants/index.js"; -import {Method, Encoding, Version} from "../../../../../src/network/reqresp/types.js"; +import {ReqRespMethod, Encoding, Version} from "../../../../../src/network/reqresp/types.js"; import {handleRequest, PerformRequestHandler} from "../../../../../src/network/reqresp/response/index.js"; import {PeersData} from "../../../../../src/network/peers/peersData.js"; import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; @@ -23,7 +23,7 @@ describe("network / reqresp / response / handleRequest", () => { const testCases: { id: string; - method: Method; + method: ReqRespMethod; encoding: Encoding; requestChunks: Uint8ArrayList[]; performRequestHandler: PerformRequestHandler; @@ -32,7 +32,7 @@ describe("network / reqresp / response / handleRequest", () => { }[] = [ { id: "Yield two chunks, then throw", - method: Method.Ping, + method: ReqRespMethod.Ping, encoding: Encoding.SSZ_SNAPPY, requestChunks: sszSnappyPing.chunks, // Request Ping: BigInt(1) performRequestHandler: async function* () { diff --git a/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts b/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts index a62f4150c242..f05a502fb7c8 100644 --- a/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts @@ -5,7 +5,7 @@ import {createSecp256k1PeerId} from "@libp2p/peer-id-factory"; import {IPeerRpcScoreStore, PeerAction, PeerRpcScoreStore} from "../../../../../src/network/index.js"; import {defaultNetworkOptions} from "../../../../../src/network/options.js"; import {InboundRateLimiter} from "../../../../../src/network/reqresp/response/rateLimiter.js"; -import {Method, RequestTypedContainer} from "../../../../../src/network/reqresp/types.js"; +import {ReqRespMethod, RequestTypedContainer} from "../../../../../src/network/reqresp/types.js"; import {testLogger} from "../../../../utils/logger.js"; describe("ResponseRateLimiter", () => { @@ -39,7 +39,7 @@ describe("ResponseRateLimiter", () => { */ it("requestCountPeerLimit", async () => { const peerId = await createSecp256k1PeerId(); - const requestTyped = {method: Method.Ping, body: BigInt(1)} as RequestTypedContainer; + const requestTyped = {method: ReqRespMethod.Ping, body: BigInt(1)} as RequestTypedContainer; for (let i = 0; i < defaultNetworkOptions.requestCountPeerLimit; i++) { expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); } @@ -71,12 +71,18 @@ describe("ResponseRateLimiter", () => { */ it("blockCountTotalTracker", async () => { const blockCount = Math.floor(defaultNetworkOptions.blockCountTotalLimit / 2); - const requestTyped = {method: Method.BeaconBlocksByRange, body: {count: blockCount}} as RequestTypedContainer; + const requestTyped = { + method: ReqRespMethod.BeaconBlocksByRange, + body: {count: blockCount}, + } as RequestTypedContainer; for (let i = 0; i < 2; i++) { expect(inboundRateLimiter.allowRequest(await createSecp256k1PeerId(), requestTyped)).to.equal(true); } - const oneBlockRequestTyped = {method: Method.BeaconBlocksByRoot, body: [Buffer.alloc(32)]} as RequestTypedContainer; + const oneBlockRequestTyped = { + method: ReqRespMethod.BeaconBlocksByRoot, + body: [Buffer.alloc(32)], + } as RequestTypedContainer; expect(inboundRateLimiter.allowRequest(await createSecp256k1PeerId(), oneBlockRequestTyped)).to.equal(false); expect(peerRpcScoresStub.applyAction).not.to.be.calledOnce; @@ -97,13 +103,19 @@ describe("ResponseRateLimiter", () => { */ it("blockCountPeerLimit", async () => { const blockCount = Math.floor(defaultNetworkOptions.blockCountPeerLimit / 2); - const requestTyped = {method: Method.BeaconBlocksByRange, body: {count: blockCount}} as RequestTypedContainer; + const requestTyped = { + method: ReqRespMethod.BeaconBlocksByRange, + body: {count: blockCount}, + } as RequestTypedContainer; const peerId = await createSecp256k1PeerId(); for (let i = 0; i < 2; i++) { expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); } const peerId2 = await createSecp256k1PeerId(); - const oneBlockRequestTyped = {method: Method.BeaconBlocksByRoot, body: [Buffer.alloc(32)]} as RequestTypedContainer; + const oneBlockRequestTyped = { + method: ReqRespMethod.BeaconBlocksByRoot, + body: [Buffer.alloc(32)], + } as RequestTypedContainer; // it's ok to request blocks for another peer expect(inboundRateLimiter.allowRequest(peerId2, oneBlockRequestTyped)).to.equal(true); // not ok for the same peer id as it reached the limit @@ -123,7 +135,7 @@ describe("ResponseRateLimiter", () => { const peerId = await createSecp256k1PeerId(); const pruneStub = sandbox.stub(inboundRateLimiter, "pruneByPeerIdStr" as keyof InboundRateLimiter); inboundRateLimiter.start(); - const requestTyped = {method: Method.Ping, body: BigInt(1)} as RequestTypedContainer; + const requestTyped = {method: ReqRespMethod.Ping, body: BigInt(1)} as RequestTypedContainer; expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); // no request is made in 5 minutes @@ -148,7 +160,7 @@ describe("ResponseRateLimiter", () => { peerRpcScores: peerRpcScoresStub, metrics: null, }); - const requestTyped = {method: Method.BeaconBlocksByRoot, body: [Buffer.alloc(32)]} as RequestTypedContainer; + const requestTyped = {method: ReqRespMethod.BeaconBlocksByRoot, body: [Buffer.alloc(32)]} as RequestTypedContainer; // Make it full: every 1/2s add a new request for all peers for (let i = 0; i < 1000; i++) { for (const peerId of peerIds) { diff --git a/packages/beacon-node/test/unit/network/util.test.ts b/packages/beacon-node/test/unit/network/util.test.ts index 938ac05ee831..6d1319df6c94 100644 --- a/packages/beacon-node/test/unit/network/util.test.ts +++ b/packages/beacon-node/test/unit/network/util.test.ts @@ -4,7 +4,7 @@ import {createSecp256k1PeerId} from "@libp2p/peer-id-factory"; import {config} from "@lodestar/config/default"; import {ForkName} from "@lodestar/params"; import {ENR} from "@chainsafe/discv5"; -import {Method, Version, Encoding, Protocol, protocolPrefix} from "../../../src/network/reqresp/types.js"; +import {ReqRespMethod, Version, Encoding, Protocol, protocolPrefix} from "../../../src/network/reqresp/types.js"; import {defaultNetworkOptions} from "../../../src/network/options.js"; import {formatProtocolID} from "../../../src/network/reqresp/utils/index.js"; import {createNodeJsLibp2p, isLocalMultiAddr} from "../../../src/network/index.js"; @@ -24,19 +24,19 @@ describe("Test isLocalMultiAddr", () => { describe("ReqResp protocolID parse / render", () => { const testCases: { - method: Method; + method: ReqRespMethod; version: Version; encoding: Encoding; protocolId: string; }[] = [ { - method: Method.Status, + method: ReqRespMethod.Status, version: Version.V1, encoding: Encoding.SSZ_SNAPPY, protocolId: "/eth2/beacon_chain/req/status/1/ssz_snappy", }, { - method: Method.BeaconBlocksByRange, + method: ReqRespMethod.BeaconBlocksByRange, version: Version.V2, encoding: Encoding.SSZ_SNAPPY, protocolId: "/eth2/beacon_chain/req/beacon_blocks_by_range/2/ssz_snappy", @@ -60,7 +60,7 @@ describe("ReqResp protocolID parse / render", () => { // +1 for the first "/" const suffix = protocolId.slice(protocolPrefix.length + 1); - const [method, version, encoding] = suffix.split("/") as [Method, Version, Encoding]; + const [method, version, encoding] = suffix.split("/") as [ReqRespMethod, Version, Encoding]; return {method, version, encoding}; } }); diff --git a/packages/reqresp/.babel-register b/packages/reqresp/.babel-register deleted file mode 100644 index 35d91b6f385e..000000000000 --- a/packages/reqresp/.babel-register +++ /dev/null @@ -1,15 +0,0 @@ -/* - See - https://github.com/babel/babel/issues/8652 - https://github.com/babel/babel/pull/6027 - Babel isn't currently configured by default to read .ts files and - can only be configured to do so via cli or configuration below. - - This file is used by mocha to interpret test files using a properly - configured babel. - - This can (probably) be removed in babel 8.x. -*/ -require('@babel/register')({ - extensions: ['.ts'], -}) diff --git a/packages/reqresp/.gitignore b/packages/reqresp/.gitignore deleted file mode 100644 index 668e0a04c0b4..000000000000 --- a/packages/reqresp/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules/ -lib -.nyc_output/ -coverage/** -.DS_Store -*.swp -.idea -yarn-error.log -package-lock.json -dist* diff --git a/packages/reqresp/.mocharc.yaml b/packages/reqresp/.mocharc.yaml deleted file mode 100644 index f9375365e517..000000000000 --- a/packages/reqresp/.mocharc.yaml +++ /dev/null @@ -1,8 +0,0 @@ -colors: true -timeout: 2000 -exit: true -extension: ["ts"] -require: - - ./test/setup.ts -node-option: - - "loader=ts-node/esm" diff --git a/packages/reqresp/README.md b/packages/reqresp/README.md index 059d5df34b5f..93c124dc8537 100644 --- a/packages/reqresp/README.md +++ b/packages/reqresp/README.md @@ -12,17 +12,21 @@ Typescript REST client for the [Ethereum Consensus API spec](https://github.com/ ## Usage ```typescript -import {getClient} from "@lodestar/api"; -import {config} from "@lodestar/config/default"; - -const api = getClient({baseUrl: "http://localhost:9596"}, {config}); - -api.beacon - .getStateValidator( - "head", - "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95" - ) - .then((res) => console.log("Your balance is:", res.data.balance)); +import {Libp2p} from "libp2p"; +import {EncodedPayloadType, ReqResp} from "@lodestar/reqresp"; +import {Ping} from "@lodestar/reqresp/messages"; +import {ILogger} from "@lodestar/utils"; + +async function getReqResp(libp2p: Libp2p, logger: ILogger): Promise { + const reqResp = new ReqResp({libp2p, logger, metrics: null}); + + // Register a PONG handler to respond with caller's Ping request + reqResp.registerProtocol( + Ping(async function* (req: bigint) { + yield {type: EncodedPayloadType.ssz, data: req}; + }) + ); +} ``` ## Prerequisites diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index 69279321b5e1..8f19cf2cf8a3 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -59,17 +59,14 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { + "@chainsafe/snappy-stream": "5.1.1", "@libp2p/interface-connection": "^3.0.2", "@libp2p/interface-peer-id": "^1.0.4", - "strict-event-emitter-types": "^2.0.0", "@lodestar/types": "^1.2.1", "@lodestar/utils": "^1.2.1", "@lodestar/params": "^1.2.1", "@lodestar/config": "^1.2.1", - "@chainsafe/discv5": "^1.4.0", - "@chainsafe/ssz": "^0.9.2", "stream-to-it": "^0.2.0", - "@chainsafe/snappy-stream": "5.1.1", "varint": "^6.0.0", "snappyjs": "^0.7.0", "uint8arraylist": "^2.3.2" diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index 233a9c467037..5a56cb2cb0db 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -3,7 +3,6 @@ import {Connection, Stream} from "@libp2p/interface-connection"; import {PeerId} from "@libp2p/interface-peer-id"; import {Libp2p} from "libp2p"; import {ILogger} from "@lodestar/utils"; -import {IBeaconConfig} from "@lodestar/config"; import {getMetrics, Metrics, MetricsRegister} from "./metrics.js"; import {RequestError, RequestErrorCode, sendRequest, SendRequestOpts} from "./request/index.js"; import {handleRequest} from "./response/index.js"; @@ -16,7 +15,6 @@ export const DEFAULT_PROTOCOL_PREFIX = "/eth2/beacon_chain/req"; export interface ReqRespProtocolModules { libp2p: Libp2p; logger: ILogger; - config: IBeaconConfig; metrics: Metrics | null; } @@ -32,7 +30,7 @@ export interface ReqRespOpts extends SendRequestOpts { * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ -export abstract class ReqResp { +export class ReqResp { private readonly libp2p: Libp2p; private readonly logger: ILogger; private readonly metrics: Metrics | null; @@ -44,7 +42,7 @@ export abstract class ReqResp { /** `${protocolPrefix}/${method}/${version}/${encoding}` */ private readonly supportedProtocols = new Map(); - constructor(modules: ReqRespProtocolModules, private readonly opts: ReqRespOpts) { + constructor(modules: ReqRespProtocolModules, private readonly opts: ReqRespOpts = {}) { this.libp2p = modules.libp2p; this.logger = modules.logger; this.metrics = modules.metrics ? getMetrics((modules.metrics as unknown) as MetricsRegister) : null; @@ -137,7 +135,7 @@ export abstract class ReqResp { this.metrics?.incomingRequests.inc({method}); const timer = this.metrics?.incomingRequestHandlerTime.startTimer({method}); - this.onIncomingRequest?.(peerId, method); + this.onIncomingRequest?.(peerId, protocol as ProtocolDefinition); try { await handleRequest({ @@ -162,7 +160,7 @@ export abstract class ReqResp { }; } - protected onIncomingRequest(_peerId: PeerId, _method: string): void { + protected onIncomingRequest(_peerId: PeerId, _protocol: ProtocolDefinition): void { // Override } diff --git a/packages/reqresp/src/encoders/responseDecode.ts b/packages/reqresp/src/encoders/responseDecode.ts index bd18e6161b87..4d4ae0aa30a6 100644 --- a/packages/reqresp/src/encoders/responseDecode.ts +++ b/packages/reqresp/src/encoders/responseDecode.ts @@ -1,10 +1,15 @@ import {Uint8ArrayList} from "uint8arraylist"; import {ForkName} from "@lodestar/params"; -import {Type} from "@chainsafe/ssz"; import {BufferedSource, decodeErrorMessage} from "../utils/index.js"; import {readEncodedPayload} from "../encodingStrategies/index.js"; import {ResponseError} from "../response/index.js"; -import {ContextBytesType, CONTEXT_BYTES_FORK_DIGEST_LENGTH, ContextBytesFactory, ProtocolDefinition} from "../types.js"; +import { + ContextBytesType, + CONTEXT_BYTES_FORK_DIGEST_LENGTH, + ContextBytesFactory, + ProtocolDefinition, + TypeSerializer, +} from "../types.js"; import {RespStatus} from "../interface.js"; /** @@ -58,7 +63,7 @@ export function responseDecode( } const forkName = await readContextBytes(protocol.contextBytes, bufferedSource); - const type = protocol.responseType(forkName) as Type; + const type = protocol.responseType(forkName) as TypeSerializer; yield await readEncodedPayload(bufferedSource, protocol.encoding, type); diff --git a/packages/reqresp/src/encodingStrategies/index.ts b/packages/reqresp/src/encodingStrategies/index.ts index 610142e633b0..12f8ab75be61 100644 --- a/packages/reqresp/src/encodingStrategies/index.ts +++ b/packages/reqresp/src/encodingStrategies/index.ts @@ -1,5 +1,4 @@ -import {Type} from "@chainsafe/ssz"; -import {Encoding, EncodedPayload} from "../types.js"; +import {Encoding, EncodedPayload, TypeSerializer} from "../types.js"; import {BufferedSource} from "../utils/index.js"; import {readSszSnappyPayload} from "./sszSnappy/decode.js"; import {writeSszSnappyPayload} from "./sszSnappy/encode.js"; @@ -18,7 +17,7 @@ import {writeSszSnappyPayload} from "./sszSnappy/encode.js"; export async function readEncodedPayload( bufferedSource: BufferedSource, encoding: Encoding, - type: Type + type: TypeSerializer ): Promise { switch (encoding) { case Encoding.SSZ_SNAPPY: @@ -38,7 +37,7 @@ export async function readEncodedPayload( export async function* writeEncodedPayload( chunk: EncodedPayload, encoding: Encoding, - serializer: Type + serializer: TypeSerializer ): AsyncGenerator { switch (encoding) { case Encoding.SSZ_SNAPPY: diff --git a/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts index 4abe4e19c628..79e176d1d9ab 100644 --- a/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts +++ b/packages/reqresp/src/encodingStrategies/sszSnappy/decode.ts @@ -1,13 +1,12 @@ import varint from "varint"; import {Uint8ArrayList} from "uint8arraylist"; -import {Type} from "@chainsafe/ssz"; import {BufferedSource} from "../../utils/index.js"; +import {TypeSerializer} from "../../types.js"; import {SnappyFramesUncompress} from "./snappyFrames/uncompress.js"; import {maxEncodedLen} from "./utils.js"; import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; export const MAX_VARINT_BYTES = 10; -export type TypeRead = Pick, "minSize" | "maxSize" | "deserialize">; /** * ssz_snappy encoding strategy reader. @@ -16,7 +15,7 @@ export type TypeRead = Pick, "minSize" | "maxSize" | "deserialize">; * | * ``` */ -export async function readSszSnappyPayload(bufferedSource: BufferedSource, type: TypeRead): Promise { +export async function readSszSnappyPayload(bufferedSource: BufferedSource, type: TypeSerializer): Promise { const sszDataLength = await readSszSnappyHeader(bufferedSource, type); const bytes = await readSszSnappyBody(bufferedSource, sszDataLength); @@ -29,7 +28,7 @@ export async function readSszSnappyPayload(bufferedSource: BufferedSource, ty */ export async function readSszSnappyHeader( bufferedSource: BufferedSource, - type: Pick, "minSize" | "maxSize"> + type: Pick, "minSize" | "maxSize"> ): Promise { for await (const buffer of bufferedSource) { // Get next bytes if empty @@ -126,7 +125,7 @@ export async function readSszSnappyBody(bufferedSource: BufferedSource, sszDataL * Deseralizes SSZ body. * `isSszTree` option allows the SignedBeaconBlock type to be deserialized as a tree */ -function deserializeSszBody(bytes: Uint8Array, type: TypeRead): T { +function deserializeSszBody(bytes: Uint8Array, type: TypeSerializer): T { try { return type.deserialize(bytes); } catch (e) { diff --git a/packages/reqresp/src/encodingStrategies/sszSnappy/encode.ts b/packages/reqresp/src/encodingStrategies/sszSnappy/encode.ts index 883c01e29e20..a8842e5e7979 100644 --- a/packages/reqresp/src/encodingStrategies/sszSnappy/encode.ts +++ b/packages/reqresp/src/encodingStrategies/sszSnappy/encode.ts @@ -1,8 +1,7 @@ import varint from "varint"; import {source} from "stream-to-it"; -import {Type} from "@chainsafe/ssz"; import snappy from "@chainsafe/snappy-stream"; -import {EncodedPayload, EncodedPayloadType} from "../../types.js"; +import {EncodedPayload, EncodedPayloadType, TypeSerializer} from "../../types.js"; import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; /** @@ -12,7 +11,10 @@ import {SszSnappyError, SszSnappyErrorCode} from "./errors.js"; * | * ``` */ -export async function* writeSszSnappyPayload(body: EncodedPayload, type: Type): AsyncGenerator { +export async function* writeSszSnappyPayload( + body: EncodedPayload, + type: TypeSerializer +): AsyncGenerator { const serializedBody = serializeSszBody(body, type); yield* encodeSszSnappy(serializedBody); @@ -45,7 +47,7 @@ export async function* encodeSszSnappy(bytes: Buffer): AsyncGenerator { /** * Returns SSZ serialized body. Wrapps errors with SszSnappyError.SERIALIZE_ERROR */ -function serializeSszBody(chunk: EncodedPayload, type: Type): Buffer { +function serializeSszBody(chunk: EncodedPayload, type: TypeSerializer): Buffer { switch (chunk.type) { case EncodedPayloadType.bytes: return chunk.bytes as Buffer; diff --git a/packages/reqresp/src/messages/Ping.ts b/packages/reqresp/src/messages/Ping.ts index 3f26da6a5f0f..39276814c230 100644 --- a/packages/reqresp/src/messages/Ping.ts +++ b/packages/reqresp/src/messages/Ping.ts @@ -1,8 +1,8 @@ import {phase0, ssz} from "@lodestar/types"; -import {ContextBytesType, Encoding, ProtocolDefinitionGenerator} from "../types.js"; +import {ContextBytesType, Encoding, ProtocolDefinition, ReqRespHandler} from "../types.js"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const Ping: ProtocolDefinitionGenerator = (modules, handler) => { +export function Ping(handler: ReqRespHandler): ProtocolDefinition { return { method: "ping", version: 1, @@ -13,4 +13,4 @@ export const Ping: ProtocolDefinitionGenerator = (modu renderRequestBody: (req) => req.toString(10), contextBytes: {type: ContextBytesType.Empty}, }; -}; +} diff --git a/packages/reqresp/src/rate_limiter/index.ts b/packages/reqresp/src/rate_limiter/index.ts deleted file mode 100644 index f85a8147f3aa..000000000000 --- a/packages/reqresp/src/rate_limiter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./RateLimiter.js"; -export * from "./RateTracker.js"; diff --git a/packages/reqresp/src/sharedTypes.ts b/packages/reqresp/src/sharedTypes.ts deleted file mode 100644 index 73255bbbeb26..000000000000 --- a/packages/reqresp/src/sharedTypes.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {Encoding} from "./types.js"; - -// These interfaces are shared among beacon-node package. -export enum ScoreState { - /** We are content with the peers performance. We permit connections and messages. */ - Healthy = "Healthy", - /** The peer should be disconnected. We allow re-connections if the peer is persistent */ - Disconnected = "Disconnected", - /** The peer is banned. We disallow new connections until it's score has decayed into a tolerable threshold */ - Banned = "Banned", -} - -export enum RelevantPeerStatus { - Unknown = "unknown", - relevant = "relevant", - irrelevant = "irrelevant", -} - -export enum ClientKind { - Lighthouse = "Lighthouse", - Nimbus = "Nimbus", - Teku = "Teku", - Prysm = "Prysm", - Lodestar = "Lodestar", - Unknown = "Unknown", -} - -export interface PeersData { - getAgentVersion(peerIdStr: string): string; - getPeerKind(peerIdStr: string): ClientKind; - getEncodingPreference(peerIdStr: string): Encoding | null; - setEncodingPreference(peerIdStr: string, encoding: Encoding): void; -} diff --git a/packages/reqresp/src/types.ts b/packages/reqresp/src/types.ts index 7ce98774fd40..88410ffce644 100644 --- a/packages/reqresp/src/types.ts +++ b/packages/reqresp/src/types.ts @@ -1,5 +1,4 @@ import {PeerId} from "@libp2p/interface-peer-id"; -import {Type} from "@chainsafe/ssz"; import {IBeaconConfig, IForkConfig, IForkDigestContext} from "@lodestar/config"; import {ForkName} from "@lodestar/params"; import {Slot} from "@lodestar/types"; @@ -31,9 +30,9 @@ export interface ProtocolDefinition { encoding: Encoding; handler: ReqRespHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any - requestType: (fork: ForkName) => Type | null; + requestType: (fork: ForkName) => TypeSerializer | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any - responseType: (fork: ForkName) => Type; + responseType: (fork: ForkName) => TypeSerializer; renderRequestBody?: (request: Req) => string; contextBytes: ContextBytesFactory; } @@ -84,3 +83,13 @@ export enum LightClientServerErrorCode { export type LightClientServerErrorType = {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}; export class LightClientServerError extends LodestarError {} + +/** + * Lightweight interface of ssz Type + */ +export interface TypeSerializer { + serialize(data: T): Uint8Array; + deserialize(bytes: Uint8Array): T; + maxSize: number; + minSize: number; +} From cf9f5938eed8e87e9fe38790025ce6d11d1c285d Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 18 Nov 2022 18:09:05 +0100 Subject: [PATCH 10/23] Fix metrics type --- packages/beacon-node/src/network/events.ts | 2 +- packages/beacon-node/src/network/interface.ts | 2 +- packages/beacon-node/src/network/network.ts | 4 +- .../beacon-node/src/network/reqresp/index.ts | 8 +-- packages/reqresp/src/ReqResp.ts | 4 +- packages/reqresp/src/metrics.ts | 53 +++++++++---------- 6 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/beacon-node/src/network/events.ts b/packages/beacon-node/src/network/events.ts index d527c1587879..60d072432b1a 100644 --- a/packages/beacon-node/src/network/events.ts +++ b/packages/beacon-node/src/network/events.ts @@ -2,7 +2,7 @@ import {EventEmitter} from "events"; import {PeerId} from "@libp2p/interface-peer-id"; import StrictEventEmitter from "strict-event-emitter-types"; import {allForks, phase0} from "@lodestar/types"; -import {RequestTypedContainer} from "./reqresp/types.js"; +import {RequestTypedContainer} from "./reqresp/index.js"; export enum NetworkEvent { /** A relevant peer has connected or has been re-STATUS'd */ diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index db4a65d0bd15..ff6cc862cb32 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -2,11 +2,11 @@ import {Connection} from "@libp2p/interface-connection"; import {Multiaddr} from "@multiformats/multiaddr"; import {PeerId} from "@libp2p/interface-peer-id"; import {Discv5, ENR} from "@chainsafe/discv5"; -import {IReqRespBeaconNode} from "./reqresp/index.js"; import {INetworkEventBus} from "./events.js"; import {Eth2Gossipsub} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {PeerAction} from "./peers/index.js"; +import {IReqRespBeaconNode} from "./reqresp/index.js"; import {IAttnetsService, ISubnetsService, CommitteeSubscription} from "./subnets/index.js"; export type PeerSearchOptions = { diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index c08b77ad13e6..6067e348024c 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -13,6 +13,7 @@ import {IMetrics} from "../metrics/index.js"; import {ChainEvent, IBeaconChain, IBeaconClock} from "../chain/index.js"; import {INetworkOptions} from "./options.js"; import {INetwork} from "./interface.js"; +import {IReqRespBeaconNode, ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/index.js"; import {Eth2Gossipsub, getGossipHandlers, GossipHandlers, GossipType} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {FORK_EPOCH_LOOKAHEAD, getActiveForks} from "./forks.js"; @@ -22,7 +23,6 @@ import {INetworkEventBus, NetworkEventBus} from "./events.js"; import {AttnetsService, CommitteeSubscription, SyncnetsService} from "./subnets/index.js"; import {PeersData} from "./peers/peersData.js"; import {getConnectionsMap, isPublishToZeroPeersError} from "./util.js"; -import {IReqRespBeaconNode, ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/index.js"; interface INetworkModules { config: IBeaconConfig; @@ -76,7 +76,7 @@ export class Network implements INetwork { config, libp2p, reqRespHandlers, - metadataController: metadata, + metadata, peerRpcScores, logger, networkEventBus, diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index cff2dd8cc273..4d7c8e0783ef 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -48,7 +48,7 @@ export interface ReqRespBeaconNodeModules { config: IBeaconConfig; metrics: IMetrics | null; reqRespHandlers: ReqRespHandlers; - metadataController: MetadataController; + metadata: MetadataController; peerRpcScores: IPeerRpcScoreStore; networkEventBus: INetworkEventBus; } @@ -79,14 +79,14 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { private readonly peersData: PeersData; constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) { - const {reqRespHandlers, networkEventBus, peersData, peerRpcScores, metadataController, logger, metrics} = modules; + const {reqRespHandlers, networkEventBus, peersData, peerRpcScores, metadata, logger, metrics} = modules; - super({...modules, metrics: metrics?.reqResp ?? null}, options); + super({...modules, metricsRegister: metrics?.register ?? null}, options); this.reqRespHandlers = reqRespHandlers; this.peerRpcScores = peerRpcScores; this.peersData = peersData; - this.metadataController = metadataController; + this.metadataController = metadata; this.networkEventBus = networkEventBus; this.inboundRateLimiter = new InboundRateLimiter(options, { logger, diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index 5a56cb2cb0db..8ee3f8be7242 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -15,7 +15,7 @@ export const DEFAULT_PROTOCOL_PREFIX = "/eth2/beacon_chain/req"; export interface ReqRespProtocolModules { libp2p: Libp2p; logger: ILogger; - metrics: Metrics | null; + metricsRegister: MetricsRegister | null; } export interface ReqRespOpts extends SendRequestOpts { @@ -45,7 +45,7 @@ export class ReqResp { constructor(modules: ReqRespProtocolModules, private readonly opts: ReqRespOpts = {}) { this.libp2p = modules.libp2p; this.logger = modules.logger; - this.metrics = modules.metrics ? getMetrics((modules.metrics as unknown) as MetricsRegister) : null; + this.metrics = modules.metricsRegister ? getMetrics(modules.metricsRegister) : null; this.protocolPrefix = opts.protocolPrefix ?? DEFAULT_PROTOCOL_PREFIX; } diff --git a/packages/reqresp/src/metrics.ts b/packages/reqresp/src/metrics.ts index d9683b84ed92..4c897c780f43 100644 --- a/packages/reqresp/src/metrics.ts +++ b/packages/reqresp/src/metrics.ts @@ -1,50 +1,49 @@ -type LabelsGeneric = Record; -type CollectFn = (metric: Gauge) => void; +type LabelValues = Partial>; -interface Gauge { +interface Gauge { // Sorry for this mess, `prom-client` API choices are not great // If the function signature was `inc(value: number, labels?: Labels)`, this would be simpler inc(value?: number): void; - inc(labels: Labels, value?: number): void; - inc(arg1?: Labels | number, arg2?: number): void; + inc(labels: LabelValues, value?: number): void; + inc(arg1?: LabelValues | number, arg2?: number): void; dec(value?: number): void; - dec(labels: Labels, value?: number): void; - dec(arg1?: Labels | number, arg2?: number): void; + dec(labels: LabelValues, value?: number): void; + dec(arg1?: LabelValues | number, arg2?: number): void; set(value: number): void; - set(labels: Labels, value: number): void; - set(arg1?: Labels | number, arg2?: number): void; + set(labels: LabelValues, value: number): void; + set(arg1?: LabelValues | number, arg2?: number): void; - addCollect(collectFn: CollectFn): void; + addCollect: (collectFn: () => void) => void; } -interface Histogram { - startTimer(arg1?: Labels): (labels?: Labels) => number; +interface Histogram { + startTimer(arg1?: LabelValues): (labels?: LabelValues) => number; observe(value: number): void; - observe(labels: Labels, values: number): void; - observe(arg1: Labels | number, arg2?: number): void; + observe(labels: LabelValues, values: number): void; + observe(arg1: LabelValues | number, arg2?: number): void; reset(): void; } -type GaugeConfig = { +type GaugeConfig = { name: string; help: string; - labelNames?: keyof Labels extends string ? (keyof Labels)[] : undefined; + labelNames?: T[]; }; -type HistogramConfig = { +type HistogramConfig = { name: string; help: string; - labelNames?: (keyof Labels)[]; + labelNames?: T[]; buckets?: number[]; }; export interface MetricsRegister { - gauge(config: GaugeConfig): Gauge; - histogram(config: HistogramConfig): Histogram; + gauge(config: GaugeConfig): Gauge; + histogram(config: HistogramConfig): Histogram; } export type Metrics = ReturnType; @@ -66,34 +65,34 @@ export function getMetrics(register: MetricsRegister) { // Using function style instead of class to prevent having to re-declare all MetricsPrometheus types. return { - outgoingRequests: register.gauge<{method: string}>({ + outgoingRequests: register.gauge<"method">({ name: "beacon_reqresp_outgoing_requests_total", help: "Counts total requests done per method", labelNames: ["method"], }), - outgoingRequestRoundtripTime: register.histogram<{method: string}>({ + outgoingRequestRoundtripTime: register.histogram<"method">({ name: "beacon_reqresp_outgoing_request_roundtrip_time_seconds", help: "Histogram of outgoing requests round-trip time", labelNames: ["method"], buckets: [0.1, 0.2, 0.5, 1, 5, 15, 60], }), - outgoingErrors: register.gauge<{method: string}>({ + outgoingErrors: register.gauge<"method">({ name: "beacon_reqresp_outgoing_requests_error_total", help: "Counts total failed requests done per method", labelNames: ["method"], }), - incomingRequests: register.gauge<{method: string}>({ + incomingRequests: register.gauge<"method">({ name: "beacon_reqresp_incoming_requests_total", help: "Counts total responses handled per method", labelNames: ["method"], }), - incomingRequestHandlerTime: register.histogram<{method: string}>({ + incomingRequestHandlerTime: register.histogram<"method">({ name: "beacon_reqresp_incoming_request_handler_time_seconds", help: "Histogram of incoming requests internal handling time", labelNames: ["method"], buckets: [0.1, 0.2, 0.5, 1, 5], }), - incomingErrors: register.gauge<{method: string}>({ + incomingErrors: register.gauge<"method">({ name: "beacon_reqresp_incoming_requests_error_total", help: "Counts total failed responses handled per method", labelNames: ["method"], @@ -102,7 +101,7 @@ export function getMetrics(register: MetricsRegister) { name: "beacon_reqresp_dial_errors_total", help: "Count total dial errors", }), - rateLimitErrors: register.gauge<{tracker: string}>({ + rateLimitErrors: register.gauge<"tracker">({ name: "beacon_reqresp_rate_limiter_errors_total", help: "Count rate limiter errors", labelNames: ["tracker"], From 39899ef225586adc223ee1f2d3c83ac0be26ef9c Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Fri, 18 Nov 2022 18:31:30 +0100 Subject: [PATCH 11/23] Update status handler to use same pattern --- packages/beacon-node/src/network/reqresp/handlers/index.ts | 5 +++-- .../beacon-node/src/network/reqresp/handlers/status.ts | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 packages/beacon-node/src/network/reqresp/handlers/status.ts diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index d1c6aa9f2efb..43138f949500 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -1,4 +1,4 @@ -import {EncodedPayloadType, HandlerTypeFromMessage} from "@lodestar/reqresp"; +import {HandlerTypeFromMessage} from "@lodestar/reqresp"; import messages from "@lodestar/reqresp/messages"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; @@ -8,6 +8,7 @@ import {onLightClientBootstrap} from "./lightClientBootstrap.js"; import {onLightClientFinalityUpdate} from "./lightClientFinalityUpdate.js"; import {onLightClientOptimisticUpdate} from "./lightClientOptimisticUpdate.js"; import {onLightClientUpdatesByRange} from "./lightClientUpdatesByRange.js"; +import {onStatus} from "./status.js"; export interface ReqRespHandlers { onStatus: HandlerTypeFromMessage; @@ -25,7 +26,7 @@ export interface ReqRespHandlers { export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconChain}): ReqRespHandlers { return { async *onStatus() { - yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; + yield* onStatus(chain); }, async *onBeaconBlocksByRange(req) { yield* onBeaconBlocksByRange(req, chain, db); diff --git a/packages/beacon-node/src/network/reqresp/handlers/status.ts b/packages/beacon-node/src/network/reqresp/handlers/status.ts new file mode 100644 index 000000000000..e05f6166c3b0 --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/handlers/status.ts @@ -0,0 +1,7 @@ +import {EncodedPayload, EncodedPayloadType} from "@lodestar/reqresp"; +import {phase0} from "@lodestar/types"; +import {IBeaconChain} from "../../../chain/index.js"; + +export async function* onStatus(chain: IBeaconChain): AsyncIterable> { + yield {type: EncodedPayloadType.ssz, data: chain.getStatus()}; +} From 971c52f1beb054db900ec0ba10033c26b3487eeb Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Fri, 18 Nov 2022 18:31:51 +0100 Subject: [PATCH 12/23] Fix the import syntax for default imports --- packages/beacon-node/src/network/reqresp/handlers/index.ts | 2 +- packages/beacon-node/src/network/reqresp/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 43138f949500..3b930283a43d 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -1,5 +1,5 @@ import {HandlerTypeFromMessage} from "@lodestar/reqresp"; -import messages from "@lodestar/reqresp/messages"; +import * as messages from "@lodestar/reqresp/messages"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js"; diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index 4d7c8e0783ef..e8b6111b8a30 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -17,7 +17,7 @@ import { RequestError, ResponseError, } from "@lodestar/reqresp"; -import messages from "@lodestar/reqresp/messages"; +import * as messages from "@lodestar/reqresp/messages"; import {IMetrics} from "../../metrics/metrics.js"; import {INetworkEventBus, NetworkEvent} from "../events.js"; import {IPeerRpcScoreStore, PeerAction} from "../peers/score.js"; From 9a8f1278aeb7425c16da61cc736e490e36535981 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Fri, 18 Nov 2022 23:13:47 +0100 Subject: [PATCH 13/23] Remove the existing unit tests for reqresp temporarily --- .../sszSnappy/decode.test.ts | 25 -- .../sszSnappy/encode.test.ts | 37 -- .../serialized.ssz | Bin 139405 -> 0 bytes .../streamed.snappy | Bin 52357 -> 0 bytes .../encodingStrategies/sszSnappy/testData.ts | 31 -- .../network/reqresp/encoders/request.test.ts | 73 ---- .../reqresp/encoders/requestTypes.test.ts | 61 --- .../network/reqresp/encoders/response.test.ts | 369 ------------------ .../reqresp/encoders/responseTypes.test.ts | 76 ---- .../sszSnappy/decode.test.ts | 69 ---- .../sszSnappy/encode.test.ts | 74 ---- .../snappy-frames/uncompress.test.ts | 56 --- .../encodingStrategies/sszSnappy/testData.ts | 113 ------ .../sszSnappy/utils.test.ts | 10 - .../unit/network/reqresp/rateTracker.test.ts | 53 --- .../reqresp/request/collectResponses.test.ts | 37 -- .../request/responseTimeoutsHandler.test.ts | 144 ------- .../network/reqresp/response/index.test.ts | 92 ----- .../reqresp/response/rateLimiter.test.ts | 175 --------- .../test/unit/network/reqresp/utils.ts | 79 ---- .../assertSequentialBlocksInRange.test.ts | 84 ---- 21 files changed, 1658 deletions(-) delete mode 100644 packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts delete mode 100644 packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts delete mode 100644 packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/goerliShadowForkBlock.13249/serialized.ssz delete mode 100644 packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/goerliShadowForkBlock.13249/streamed.snappy delete mode 100644 packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/testData.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/snappy-frames/uncompress.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/testData.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/utils.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/rateTracker.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/response/index.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/utils.ts delete mode 100644 packages/beacon-node/test/unit/network/reqresp/utils/assertSequentialBlocksInRange.test.ts diff --git a/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts b/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts deleted file mode 100644 index 9371c6507fbd..000000000000 --- a/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {expect} from "chai"; -import varint from "varint"; -import {Uint8ArrayList} from "uint8arraylist"; -import {BufferedSource} from "../../../../../../src/network/reqresp/utils/index.js"; -import {readSszSnappyPayload} from "../../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; -import {isEqualSszType} from "../../../../../utils/ssz.js"; -import {arrToSource} from "../../../../../../test/unit/network/reqresp/utils.js"; -import {goerliShadowForkBlock13249} from "./testData.js"; - -describe("network / reqresp / sszSnappy / decode", () => { - describe("Test data vectors (generated in a previous version)", () => { - const testCases = [goerliShadowForkBlock13249]; - - for (const {id, type, bytes, streamedBody, body} of testCases) { - const deserializedBody = body ?? type.deserialize(Buffer.from(bytes)); - const streamedBytes = new Uint8ArrayList(Buffer.concat([Buffer.from(varint.encode(bytes.length)), streamedBody])); - - it(id, async () => { - const bufferedSource = new BufferedSource(arrToSource([streamedBytes])); - const bodyResult = await readSszSnappyPayload(bufferedSource, type); - expect(isEqualSszType(type, bodyResult, deserializedBody)).to.equal(true, "Wrong decoded body"); - }); - } - }); -}); diff --git a/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts b/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts deleted file mode 100644 index 19cc8ae40268..000000000000 --- a/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import all from "it-all"; -import {pipe} from "it-pipe"; -import {expect} from "chai"; -import varint from "varint"; - -import {allForks, ssz} from "@lodestar/types"; - -import {reqRespBlockResponseSerializer} from "../../../../../../src/network/reqresp/types.js"; -import {writeSszSnappyPayload} from "../../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; -import {RequestOrOutgoingResponseBody} from "../../../../../../src/network/reqresp/types.js"; -import {goerliShadowForkBlock13249} from "./testData.js"; - -describe("network / reqresp / sszSnappy / encode", () => { - describe("Test data vectors (generated in a previous version)", () => { - const testCases = [goerliShadowForkBlock13249]; - - for (const testCase of testCases) { - const {id, type, bytes, streamedBody, body} = testCase; - const deserializedBody = body ?? type.deserialize(Buffer.from(bytes)); - const reqrespBody = - body ?? - (type === ssz.bellatrix.SignedBeaconBlock - ? {slot: (deserializedBody as allForks.SignedBeaconBlock).message.slot, bytes} - : deserializedBody); - - it(id, async () => { - const encodedChunks = await pipe( - writeSszSnappyPayload(reqrespBody as RequestOrOutgoingResponseBody, reqRespBlockResponseSerializer), - all - ); - const encodedStream = Buffer.concat(encodedChunks); - const expectedStreamed = Buffer.concat([Buffer.from(varint.encode(bytes.length)), streamedBody]); - expect(encodedStream).to.be.deep.equal(expectedStreamed); - }); - } - }); -}); diff --git a/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/goerliShadowForkBlock.13249/serialized.ssz b/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/goerliShadowForkBlock.13249/serialized.ssz deleted file mode 100644 index 1be96160512e0c5d6759b81023c2c5705bb78fbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139405 zcmeFa1yogC^9OwCZlokGrGRvIiAYK+jg$x|DcwkigoJ=dBPA)_-Jk+WiF9{2eAg#^ z-j~Z&@&CPRecyA|f_v_nGka$L&dlDkXU_ox002-D7e@Ok$2DMs6vx=BNmT9%dm{^v zQsyH5EBzrTC-yY0#kqnKc}{x@k{F*e86rfq4<9Kk@)I7;?Co8_(I`U~=nPbLMSnG@ z-ex!|lL~9mj+7znkg&r9K;I5*X82vl~tLYHxDQ@M9i7$Nd#!j;^BYFb$BSd5Kh3C zp?I@!r&_l_q=Iq3;XcKV%voc(o9mKEo5*43t3cUj^+S?s)l_f4qz~lp<#~P8ksH(l z29WL)&;S5F3;=)%3jjdD0RX$8pCxzzU>pGe_=pGqbRYo$wa5TK5efj1i3$KrgZS$H zum{jBza5b5;T1^E*JFw};-#t*AA%+6`w#V6+S!eD6SfsM_$$iqno6G)YZ1l&aUZ1i zN+mkD(AXELqv@VA=6n?0;iD{Ta^HZmJW~ztPbAvz258?lEk7zv#7~4tM^NF4gH2kB z@-Aq@9pw*cC?Nt|UAJY?=$zINHnwJTY0aIRw>>kKS=}-1o`J7a$xVBxadxz{lW=x2 zw>D~aT6=VS0{?BNLHPW3KvMEociR65K{wc_C?(-`p{G5pjiOHgJl1vC_beq(gg@S} zun|>8s3M$`rKetvA`$U6e{Fyf|8O-@_V@uzaTFb^0$Mv8Mhuk0ts?I3u{YHOjx2hW zgI0KB0se<45B826)3*yZM$bl0=Dr+{o|U?t9IgpreB0?i`V9DgyVJoDm`To0=ZSrt z)emUtk7NsF8$MK`zQ`6;kg}ZtqA=zPZU()^c8)UFi0Iifb-jyVj74R`T`)K^G`WM6 zU14?2x%W}44>Jn(^Ua)>03wcFxmxDJykwgbx1;-H`$fz9r)O(!i(^}7vukG~<)udm z-*y`01^<&Lo$e57Gebsp@0eK6WVSl-L7<_#!m038*#(iUFW#T8r1CU8#_rIfpFfek z4u?8(&x4@I!8W|MAg%G8F4H9Ru~o&IOh#l$Bs*lSH)Jt*Rd0JmP9mwMtXlMrB+E4u znZ<9XX}Z}*iz-$KL#?;O!mL&^v?YbAu~Ud3hO%wn>LBnZzqSL0nYkY zwc}`E+z?~p7+IbqT9mvN;3o9x5~7?_#f{v+6A;9a6ZCY~XDBzg4l|NkZhd9LK||$T z0_k8&P0U=DIMRrS_v5CeS;kv~&9(QP&NTA&7DAM|ZcMN!5wnyi-sI5BBSTl*Zb<%m3bJja{c7N)= zhQmoZ?6;i;#WequPUq%RrrS<9Se;D=(>svG6zicZRhThz8gp9{CvsZmYAgF2>IUiZ zLElQ&Q3+DgsllPX2P*>4i^_P z7vLJ99zSTll7dte+PyX!3izTOED!~7Duf&Pj$Q@5%R|;%-PD>_D zPdmy_-3m@mC;s8n|EnjRHP{}2;1e7|7f-0-@&04P!f4Fa2$BjVtH?dCJYuM@YoAE+ z(+Go>7rs2SVUBw?0;t8f*_3pzF7zqcv&I$Q!D!jZ5#i$XL2Ep0uLb{*)fwV^&6j%I zEH0RVeTe?e+yF6HOiYmPqPPP9z+NB~;b0)OZLE(02K~hJ>QC)sJ8)UNqtpzgI&H|pK?1pTE>~x zV5B2_Kg`w4pCAaIZZ5E>s`F`;w6ND)rCw)SOsy1FLPCOl z@S8#FSiqlFh56SR=qY@4uHOilf0Dmm`Q!Ne(x36f?*z=>c|@TBUxO>q59mf=0O08# z-@e}TN7RK0{QBi@fv~@Q{o@y?nXkw1*Py?)0Qg%DG~mw=9SHFIdK>8Ozcm;BgX=2+ z01E{D`pWq`w>#(zbO?iffWm)>{<7}57Skz6Mh)Ta+lVfHZEJ$33>LXf zV@USFR~`TW+wr##IVj%WKK&ZR^}e1QgL+8&QYQGfi(r0vEMo{Zk8y~Ltw%3R%+?17 zP|u7M>mf@JOd>#KV5#bVZG!;BD+jW4VNj=OL95LJN&o;!2&DM|3>(~2BvmgpBPM`X zQy~)&c|-hJQF|9KAOHvKTo z|DV@>@i*u;;Ds$hal3y@M>~8;SH$Os%UrTUPN5srQnr=c{w@8S^S^EL93sf#gwMVF z*F|UubU;(NyJ^f{w|~b8n_4q;!--eVC*gI>MBYMkYW2ZqVA&{rHqGn7QvW0*{(160 z_!NE9C0Zb;FE%4f={?8$R5!F??5bLwt0(~+8Nsu~u~Sc^yb=uM)x}Vtf*M=ewKAW@ z%BAV(5S#$`iKd{aNQKA5zk4HC)$807i8g}+=SwWOES8`(6Z6a>D1{Q&P1bl=+fGBA zSRzmO6Uj&x(hezB{353u-Cq|;X~eUw#NS86P&>^nYCNOZTTD`PA)gypfWQtfCu=nS(VKS$`=e>wX4}c!yLesf7=L`6qQgR@a+o+PSVe zriyNLP>E%Eu{PqJcbpErn626;XXkKd^iSa<#l4=!0FV?U?M)J~xd3$~G?M%p9O^;^ zVbCS5miN2VsIVjWtxO?6J~&po|u_jgYPA0)%}e6&z-G7jaN?o!&RkLLCV^|Z^4 zV;wtZBQiuttsEt$@}RG@{J8@bbkmN`l0jEIz7D#&j)v$>YWa)S->8R@XDCxY= z@m12H)$vo(q15sJdS8H&PPtB?5@RB&P@*;tPB_!Mz#UrqLEqbZ)shCZZa8m!NCHZ2JeFrO2FFK?*!qC={UfP=>Skh^@0OG47{6P z!1fIZlzF}QXzmq<^4X*0f9~p1__-CPZU#`I)baE>pVQ$Wtd@7G1tj`Kt?P;B3yO<1 z4-d9}ur82J2`Wo}+hu+{{OM>@+s3fusi`^Z#?(dpUuk-u%h|aWh)73CbWx^Ukq7b! zuh>DaBblz|`tbz%P_kUej~{m7tP0|VFw_HZFKiz~goKZ(Qpdf=vUS(|In^*+cQ}Mx z>u#(FSU4KdAGHPP1#W%5sC^KA!aXF8gi#q?&Ayc8I@NmGk(|C2R%f!LO`(zS8>5tW zS1;oKN*lI*dd8XEmGH8V zTq$+h`bs5ctq$}+Y&lj=Y7JV>`d!Uq!lqV_@fiK#PJlbvm(3b>VaH!!ouke4S*(VuwM z3>JpC2|BO#3|_?ll?JQTI+MfPj;o_4)!weVx~!$5rjMs`OzDzY3x6t;6nh~*epuLr zbjZZ7LGgtLBzz#zFKQfw-|fDZx6u8?UD^X>S-tgET7>~vE=p|^1JB++A#!Q!y~cSF z|5uu9=vay6!%Qx%LU-EN4?a=x5j?8!-M^M=Dbe9e8eq_OAwPcD^9$*aiCBr0f?^()H#~^B?*OCHg$>IA~tP|CP4E7?XG(nPkRx z{biJD0=dfw`X@41+5O`uhHX=byV=AS^5chvUr2{c#DR26HYhx6G!#s4dct!z&O5S= zTI$O>n*lf8>xze&qonY_OX4Rz$YNB!h}V;DGFKZ`IBh6fh8eO@l@(OVSZYKa>o(N` zNk7w}v<=Nq7!usAZ zuTj~vsSJ)KQqb&(=l|Rn0IOyU8?*D??$8rLtO!|E_7=QOHIMZClvl^kleUOIK(l7WS$$u&v+4_(FdCu&4{^kclLaZW&&uh$YLc zhPj*Dd;7)KRN(}9{tVsP#PqX+mi~P*Z=Xxzr>N2}MXPJJ@vwcIPu@z0NB$!=4l>Qipu09VBAm8k2}OpTYRlcZP)N1ZoI?#CUMWTKk{T?TEP!{ z0fzFumoFY4K&?Q+2NL}vML_`GrD>R;*pB+UjTLZbMn);=Z|Y5&u`355JY$hRM?B zvSGI_1}U8eo6~j!32A*X_@#Atnh$6fudngLeke_LjxPA;T>Hl*Nccda|D&=0_yWRD z4fEhijeigO(9!E8&Xp=_i(RZxw{RBxcvU9(9k}7}5C6gPUtc+vn#jkU zx}&$0tbyG1w9_+kpti^W#_{6y8GhKs@%4X6`>zXE|Le+sTxd@>)vdgO4}^jd^~_Y5 zb!AFYRAog-%=}2&C2Mb(9C%6mG&EPU{pZJoQA$(Xxvf7_P&q1;vDTjCq}H}Ki7Zwj zl>LV~|Mj(M?)3}H((sRgiJ0Q9V&tX^aPvY^XmRv%(QOP)B5}+A#qjL~{IHkbe*KB_ zi{ndBgOKonM88NUko{>TJg75ipOcD;6oz>pkkeJ8zuzzyxQLkkIy<13Y$50VMf_iB z^;4?77{TQlIL8fPgvmRtvW|>%9tb>VVvn7X9w!DuSYJWAU)(-)-NkeS;Kg(#aCC$h z_LVs^v_9aTFIsp!3wMMuBDnPfe6$}`fATH9IPmf#1e;vtGQiNGNyp*MSXHocz|fK8 z)3(c@<0DSN(BYqCSyHtNUgePtj(~J~%qYz`I>m@Gn{r)qohR>0?PuBWZNnGF z|DZ-FNU6s|-de|vpcl+$O;Wp4G2D`xUWIlPorR)7nVk&gqM&?~p;U$7KR2Upk~v}F zwvx$F(-5yvTNfMi%(Y11?G@(^4yTZ&Q0g@zhf0l&yHz~8Z*VrREk8R%v7Wdx=)+Ls zAQEJ|B!Km~{-aOcWMAfh@2t1Ig>3NM{=AqZIR&I-f9hNgQn7SH>RPW*BMRx{`9_fj z*!+*YZ{yzi;+0^v!l2%~Gql<6yAdy%e+#Woq=z;m`W+s(4|zZDz2#kr;mO{=jS zj-Sr7S_#Kf@g;V#_U)Gm4RhLrg8G3>VORS*Y4WrRNR{bO z(4E7DM^5gjW#jc^J-?nduGv||Ws~Dyn3s6?tSH8erlv4FM&V<9_rjGc1%?44)+Sqp zGfgsh{wlXO`^EhN?J+eB!zidGUBVGp?kLIi4VlRd$_>K-?q3BXYb12Biw2Iss(~hIkhZ2G0=(74ESrbpF9+!t9lM+0Mj= z&vTxX=xi=1P@&G3Y&YfCR>gft+kJv&XVB3AJ@ACXR#<7IKyj6q2#)+Js?dW$ecs{i z^@Yaj~Z1JZkCdl$%LmC zjW%7i^TSxyU#klDs8eWU7%v{LbU?d;N9=>gnGXlMIh%-H(M{EQ(m^QFs)@mq1V5?# zG~CdOiNT0(U?nCBWl;|IXh_)BSU64aU?`tWAnN1Xc8C}ojPB6rAxwXv9T_*GM{_*~ zK7*z^a-*j5=EF^S=a%MI2IV`wghY|s3ZIiv1wWUzlV&B_8WWSxHX84Uv#XVrnoHG5 zUUPZm6P?&Z9a$6?X5~4gXV(L3WWHY7()CAb*cF1Yvswm$N!%ZU`!N2f9{+%MzMtu1k0K+9J1W!CxY>Bt4+$Ri z99%^M?PpU%+Rr9Rt!K!Yg@82>0v$p=2WtFcei)sL>9FG$)8XqbrXvbpOhG=Tw5c9eJpRR|1jK3swpJvbEho_%cF>r$`+QnRwoBAc2F2t1$p4gPgf|(D; z> z5LV*|uO}4rkWYGheNRWo*PS4h)Z~X70${d{d*tn%pu!?K_d6iXJT-JWROrut_2VNh z`dXb}^bxs!*wgOG05bkn&?Yuw$Ag)Rq4`Svw_fRT^-7nIAOEd)g0SCku@PT-e*3px z3c`NQ|1tjm)>}c?FS{iElF<6Kg;>6p)oAIj!wny%S6ZmU*Mlo^0r-|=8EzC>0DfKk z{%<|lrS>}^k8g~LoQH~l4$^pZiQ+C@$!!G(ErX*cMdA|&J-F0I4izD6i3Yq)&@NJ* z^W%4}c7@Znk;-|+W4aMFo57kQDaYOQ@;`)nmkZhuy9QxHtODwtm}D%5^@SGsZw25b zD*z!rakLBZ2S6oWOeY6kOc%SD4A8E-_y%Y9i|KDKCIdz^D*J$KM6e5_%hR@!mT+p7VpX+I%; z9r;B^6UI)yCc{}jTbKD>wPh5`xH46J$Ol9v9HFysjy)5RMI^xaE!oEped@QT4vt~A z4x@j0IC30F6*`pG9)o26$V*#p0uc_Wv*6C+`Mf`>1v8={7%igfT1_@N4CRyFi}%~j z6kD`b+`VSluZ@G9K-YmEtW6DzHIV|qefsu32mV>=bDi5Gk)xGIEGGimy};14E&nf$ z8P^u~w8>OX>X8_r7DaHM1!}<1#q(yzEjGeHt$xNmFQ5U|nw@q%!+B!WUr5p%rayG6 zQgjkag8rlr#bZasB8?07FAz_3b|ULEhCjb`2b|y1u@!a(A?tx|r;XpdHse+D+&43) z`=j!7SOt~bk5rEM1l#K#pgy#FQ zX*Jo4kQ@Gp!G(vzah5r`_ep&G(*4uevtC|KO{lDj~;xzY&scr#%p8j=se+zD7! z`d+HXAJ=!Hk%(8?pZ7V#U=RY%=hSr$e7-=FI8*eH_Vq~#yGj1$J0bPrTNJ5Ok_A?0 zpXT9^oF>1lpU?aAQX8ifX5XmPKR2MBxk5^!iW*h)Nv=DZ$3pHJd&k6mj5=7Tw&Xfo zBkAHaj_wr?{bi_Bk2oc(l&}sWAl{f%1E897WuY#X!qtQ${`j*bTIl@QMArT6$&p6e zZ5XIE&hyRW89PS&=pL#`+=i%P2ZIZjvfE<6!?BBVA$27`02x~08-vs5Dp#V|)U5+! zFyCWv6Q7YtRKf87fF@d<8{0b}3g9$(r&yc&mOU0T|Jn+<4h#hw`zQ;1Ut_DpNNa_j zJfQgHHwH&0P2odsX_~`DUKONQ=&*l=$pQ^@vkSR5uY}YgjPwTAw!6zbPZTfDj%0t18m!h7L;Dd%Yz^#NC$Qb7Zo zSnB`^G&JeM8X<(UBTx&KIb!m)PW_##UxJ%H&(v!cxgjP3d2pp#-!F!=@#K%c*EA#) zp(snh%R^8YM3A}PQ+r2e8j2lPlY-PvPdxN&chnj;e1Id#4zJ-6P_Lg7+SWOU!ZYPK z$58)uZ$v$w2>m$o3R$-QhhYNJLL{;qT3w|mtN_6I{M6QB4@2VH=$&R@=|H4f^X3i8V0ZspfkNYde9UMhwX)Zu<;7HPSrn1~2ZAmK4)D%G^8 zh-C{@UO*!e$#cCTE`}IzLo4f8cs4sOEvGdKv9Cj^L-u$qM!#z+i=lj5yo3HO{+DuFx zO^;>D&MDq0M-zrcJ)ppM-b)k0Pt}!0!n}Ep@VQhj$al~6!SC@QuE#Px?qK*K$w!3J z7klc42u?hbv3Z81%WEozmliMQA8X*Pe`xHxF;BAgCYB=zS*_O#1q$BiiS5P-)hAF3 zKO4AqoZt1cuqq*Be)4QJK6I&W9JfL;V6MfHPs4EWmkh4=$w#w)O@aHIeQI6d2b#wK zFd%#nh}?QNop3_M)2dW=0M~}%M9ZG*$8&@e7{j1mcN_7HPF|r8zVXNszh{i=$oe%}#Aeol(ZaKaht$-LFW*<)@j#E3VdBkgEhpyp zN$zSpeYJ12Q!;;F25hut5yE>BGjeR>TqUqC?>CH&mj$LZ7)}RrJcz*BnW}pK>c&Ka z{oRjIgCZM*hf5e9Sz&nKM!ci$2bk176$u37ay9%kPr~iq3kb)>Q46Dpba_V5v}s*( z#M=V1bb=)IQX`%%`>-(X(}NrJ@Q;ukM8k=^Zq2Z;^Jn-p0&hye5<)Ry=Y^GzJUH5u zJ}TNBw%($@#r{bT1OHvC3_G}JOYQ3Hkd>iTYZN(^DT8+GgItA?_Bpz;S=;>O4z=kv z#$OR_O|`a=4T6cbAj!XUv_;vchbOuv1sW{8JvTzMfO`s~5?i)jX=goILJD7iV$(d6 zIye2DpIL-a3+pJKxhfhnI1eGfRG$=#-4h>p3k7$+YZM*VJ|Te;P1!P*vbj!y@RT9* zo>0iqP1R{~N2vpefnZq5HbR>7S--<03rXnC4~~n@c%y5(dYagupL)G1kdXaDbh}?qRD3FbSb;f>v(FgWXR(G zV?@&%eq`%@eoNnjpE#I!RPd##e?@e{qlyIj zs|x2&hmho7K02{4Hf}{-cLq6Y)%An|0ecN~xeZ2z$(uDutQtXrz=nw^{0gFx8}tIG zP3Wc9Yaav~Y2@BM=1qoaes!WbYY1*C7iyfYua z6p2aB)$;w>B~gk7Ghsp!dnuG+hF;eFbZqwJd^d1oSJvAgnR@hL1iC{JL+W16vp^fi z`2P5{t8g**az+2nF7C73SRaL>$=3wg^G`1qcxC4mg~9OufF@USd=?XZ>A`72i9o=v z{F1u{Hzi?o)Y^8XK5YE`OUBpYotUW!MhuL?-$c630f2?&u#tZidnLEHI}LbG3S9Vs2E#rbEeuay^Xy8ej-^!{ zfs4*tzo@4TgzmXd4&Ky`{)+Hpx;iz90L**{N&cn74_YC(6_?o<5bp1rb6#0+Ejbw? z?C5O~z0k`>mq-FWW=FQ*EYnARa$il3%9}lx#2co9wkcpwvW}CW#z!*(oUbC?eTE91 zV%!6V(P??dIhdh8@ikR70Di3Rj+R92n9;t3RNL^`|znzFK;4n(RfrbiAD@ytm@l9;toeDU*sWyl`e#?KBn9@`<;o?#4I1 ziaXns_+qH8-mlD4+hC`L!Y`(5A6RfNWafaaxOH7qLM2sNb4RHmND^lRcgM*WTU=-= znhvzDa{c_Q{FCP_Ztq6LF>A<6ENC(bM4#A0nM%mKr9v8Dg1V3DbM-GYf8V0ocW%Ef ze11#cu{SV<$b3HU4{K3A17?SUGA%C`=FGLjobN%A_VMf_btRDlB`0>}w$0)h*;ukN z42GyMZtVdE&CQON%CG%xp(R?q#+uZYt$?rN`(a213sfW4(dF?7&PZonrh3?_sYl+- zPFWghvW$cC+sbPre0zIw&gu!T!~;S5vt09n4l3I=of=zN)yR)NKDeacE`bSqA&I?| z-&VV72}9f|J@4MQT)wrxmOdJlAaQLz?mzgL5;=W|@T2f+}L$kw0 za1opKE*Vx@_aVI3ZmeI8%-GCT{-|a<7~56}b*xor<2zHoB4V3)>q(&sCSrpm|I!f~ zV^(HU+0c9)09Ry-Gd8dSPt@q~qq{YIu`hw{i*JDKA3poVtIVIx6C5z)1-WDR5?6Cd z>7Ov);H9dN+qPN&H}Z2vc$d)hF%2oqJv6aquhQZOs2VGNzBi_Dop;6<)v*0?BmY}m zFl$gqVlOrFzmu;4;PTTw?yFKG*sG_VR;j&9{^oe!qXQ^4%!P)P*IrT-@zxy&b|2wxWOJnW{_XxFJ#~|cp8u}6Zk8v zYc^9iB^r!%L6U!I>te@~eYw@m1+uOTy~Y=FwT7{XQTk1aBJ2XR^zUJTSL@5PKb#EX zkvq?HT<0twJxs17)P#S)xZ-HJE4$>Q2@bdY#o;D6X>yV$2_J0`PWU>nIcmKdem*Yj zdF?=6YLRm3SKv06AWSs~1~*9ZFO3_I$Ho)JIv)^jhW8A$+UMIs432`6+5j}*tV``E#btg6Hr}Uz?6qIN zJE>Qxg}tW7wbBYGtVf=&OWl8kbuAQd#KvteE%+ z8k|>RoAj+@%}CB5BP!@4eZ>v(uQh!4+7}#K5_=hx;$U0H zg>lKzFby`T-09%oyk8?Y_xXEw`RFN z>YT{|&Iezp8fb>U&_#~SS&^<=-$P{dil8P?^WiHSvkH$aJ!<(CW7nelNJcamAA}_T z(mp8J^m%_Q_6f)be>U4gbN*hVnuxpYgU$jMi7Q46%?~MFEzAnZlzVx@H9INQQ@^>* z@>jO!M7KRX%TuUj4G0EQ^dNvOd?ss<)S(_1km4I=gll1#bC$zph-@qMSCnZ)%E@3Xft9Ng)bfEUm^S27~Qu zOF)OmPEv16KR99sw;-oxtz|H%0W|OzQ#eR${!zAsZ8EBl$80)f^m9y9U7SoRcRFSQI{zH`){NQlB?D_&!z@Y(qt3?{dcbead!vw z)=9m%ZH;GwX>*eT5aSE}uH1gb?WPC=?CH#m0OH3E3=Hu#ZfJKq9JSPPyubm+*(HXg z*Ps=xPuyYVtL93+gxYgPR5Llh%|v@U9JXWaR{$r# zpA9)^&);9vZQZ#q_cjRSaUKyPCGiZDZU~f2u@xp6x%Qw&W|)xZKfQMEAqfw8-T5tj zN0CqwDwuV^k7|LU;tEC!xN@=5^@4iBF%pib?PoQ>&(4oAQTDor()0(Q`-S+`K%+J2 z?WcY~9+gIxD2fw7iGxv1T7D=QtS!(j8oAD z1Ly7%$sNu;!J!W`xRv0%woh8Ywx0s??SmAwIVpE7H8^{**J@2wZc)er4=q=G&3=W~ z3R3H-2!rujNb)c5wV6($;`~ZxAm9RSWS9vCOVf-g#of1VY*1{ssD}b3EYDOm;ydWk z!^Bet5Y=$gAJ(AydH{i%cIEG*@fcOX0mmAqMUk73+$v|@#&)~%>Ja%+oD28o@q9`{ zFL@}=TQ0u>IH9Y;gYIB}gCzg*z?D~XT$Lfi1p)W7L5jxtD=Y@a2M@a$bF*~Lgmy7} z&9f(-wtvp5Y6-dLvAP{f?$`9!aCdTlw~KmOw+|7d!?|xfe$NVPMbD026%79mXtJTB z?Za-q4^ES+FZ$_2eu*D$YjhE-bt=J(P9T|RnviSl+_UNGo35z&W`%`8)){yL1HcZr zb{ofyg=|73iy#6xJ~I3=?+KpM+t~<%h=i6(N?>=Rbw>)C9%ld1q&LygCHdNmmc?&s#?=)$Is1yHRUgB69dtg&CF`ZKII2|Iqqd*UFZ z;{gtuTc*%cp>M&hRf)7~$oro62@J!1A~XzNGcCzj_J+~R$vI|leUy@_l7G3iY7Us% zm!FsVeQQ<3G+Ow0K326C6)sl&JY*so8{NjhTe0h{`Q~96>$$eK(RySAGIE;BhGR^4 z=9f_`%Of^}!L3zWq#vgzQo=m>u#!of)Hg-}DtnH8$%VHmV<2kYQ<<_c29AFZZ zki=eUt;#C|?GC9b;V=4s)Q9|a{qGZ|i*a5L?yw^K#QYGVmc%$ zfh_Ja;)lXU8x9;Uhqcgtu2<%d$8(^8g`^93I?$8a4iByv2$7?Y!%!H#stcjot7E^J zqN=qi4Q{Ys1{mkR+LXK1PGWU`h3bRAfK39)8y5;qr-!$05(+uk{))kVT?{)P+~NR| z{7Vn^_n$nz+xajLG}!;_%9QT>Y(#Q|+WV}wMl`dluG4NF++lo`NzgNm1fL(1jeCeS zKK}a+|9y7Jn_4F`1||S7@#^;!k_uyyse$@%ZeHXEG~odtOlk4)g43ktmQpNK&n6DC z(w8`gahMiCO3G9?XR={v){M{r{Wm;cBlkaEZ+tXQu`AWVTk7h4^J?E2>iaM!Sq7ld zy(tT<0_N?Sk9$=Pul(7CQ~I_{;Gq=}Ip5G}lY4yvH!rv!F}+wR8%Y<1E(gu`8kZPkRZnQ1d-V&@gZ-C zrDZ6*QOh^>wg`Zoe0GSwS@-h)S|0rV5~%LRHooxrEqzZm#1~BJ_eZrD$F>HeMLIJR z@>_SWRF2-en~#G&eGU}Gc{4S##kr@Klr0@1EBkvk1WJ8)ACa%UT6G80+Sp$7F1H>o z6L2*Lmi$v%@RjMJl-)E^J3nan5G$JbBN9AEL81>)cHQ7i&CmT6BgdPY98_bsY26=A z(IjII#UTPB{2p8Gk=E6^U$VZVr~pg`39{ICbYMWL8mRoJ2c^n)htQxpEb`F=J*?&( zHGg;fRe;;XCkyHp7;HIr+=H83wfJbb`!7P|SXz%~J~ZYug#(ujucg|WjqL!ESEVySzc&)qX1rAG`*N_j5nwd@8*U|$Ni(c2H(3c zc&V9h+w=U>(9ZTRmn{DTrVR3jwE&we_lfzkS$)Qvy6Tk(4}Uk#b=vrCYJ(FFOWTrw zl@6{*)@R`Jf;64ajD_jH2pFVUVzuwb2bXW<^S{bix#W;G3?|ft zB=%B6n$}bk05|e}$b)Bx*?6{EQP&xjL*^*Gu{43)(CTNtzT}6mdQ^!FG3V%t+14Vr z)3-3ta<+~A$+=3vrHbSv{L^o}&=Qp7ts*N^MA>FAwBTd4ifv<)0WbtJsB!*^RFMe$ z$q2X!50d=Lr;6IouCHq>R)FSi0lOnRZ7k#0^4M=@NrRYzeOO`ED#7BPF;Nl3~|RfrYKtR zyXGsBIWN-_U< zojT612yHibHdw%A&>+dbbZBcZhRFc0ehM)-{cq0XJX7 zGH#=vE(^%Chi3V1#=a<#(fCR*h^^{x4$0TCBYxx<@qcGMm+dw}$NyO#m z?qctJaKRq9`2tf?h;6Lk=<^^U{t%BHO^Q~2mENd54R5%04Hlt2G3KS_i)k7#)eb)| z_50?Fl1{XbZVuerDbl_$L5@w!D%%L0Kwo2@m6uuCFudD{9mCBqTuH**6rx^o2+I5Q zCjKei>7ch6XU~s%BZeQ@Aj!VZO{7Ct)Zv+n6}iPWYS{m?Jk4Lk>KWruJZ#WN>xMBP1%iLR7YEYjJRcASl#@ z%!u@wYy3VqGqYK#A@FPbXq&3tSIv)T)AA@rG^^uzMM~MwYxF25m)^^nnG=|G!p}?n zJ~IL~#i!1My90g$R z_9Qb46c+C4IS0@qT_5)juyyX4 ze(nNW97^9DAu+yDy_!RxWQ&3DUd^2QdIsQpHcz+2++4+MsG?Fz{QEcYC84!VaAW#3 z+K&REn~78rtjHZ%&^Z6I;l`cwpOxa<5dP;@Fl=7<#^dJeu#`LQpg%cV zm?g)|FV`8i#lH5>L2OT-t^Y~g`7M1%j;I(0Ols{%wIJVo4@L|9=D=IqOj3%hdIbzj zWN-s@9%R7RXd;_4vnHvAX;KlXqAiw*$joOA7mF0C&3 zQBTInJrEIeofAa5(Xx28){8d#<>LD=F@OSHu*45F_MbQ7hb4S_=_mHD48P_7iE}6j zJa~}yr*vLE(7#RCYk0rd#D4i$se^w3Uv_(g@S(|?1m=qT3CYFnUv~eBMEK(NAyKdD z;7(>_JHa!hbYiUPuQZ#Yqaium3;}Wrs8L7^GU7JoOv642uqzTbc4B*%T+?)=>Y!lo z&Leto3j$5mN?|h@S5@v#hbsv_s%9)v1dX?z9I@p}jFC!4IL-Zv1;Nev7;VrveeT{1 zB>9(L5G;D4#X3EO2PyFB7A}hw1s$)tW{R2X>RA)60)HMbt6t>ReuUOddn!@utVuO) zW}Cyd6tVR#9!%CU*YCDU1J_fr z1Oq1MLfg=D4Lfh&4(O{1{c@bZIl;~Q1RQV?+omH`y40m{I({Z2P<9oytYOo-`uPKX zhEHD^#UFY63gB+_-_sQa0~{pzmj(_21%6BHC7~h+xH(&GqrxT=3z59zIutY1F?G@` zHsHIrw-2)8%E;%p$eNYjnf3P3P)ldd@(;F{zez80>VN|W+?KC*&^^5f8O4s(T;D$O z-h%|u=T!z4)t^Q>i|HrYnSKRu;vMXh?qGm}B>(cjU6q(6%$ojM2=}uK)4B5#KQB~N zhm>oNaL)Xk$6}3U?qJ~>7VYXC-ka2gQ_WJDllUv9JJA9C(}3gBmlPoD`RNqW4%Q=1 zRWSTNpvhtlOMCz~HaJc2Sv8!J4Ft7?jCz?&cA>`KurATPNb&KZWP60~2s4iHO$rIl z{A{fJBem{`C%){sFtg+H)kIey^{w5qjRIz1m=}V?Dq^Yx&-%^f)6`FEcE=%XKFKaD z;DS4WZpV1b*auE}??;+@ufDp)Zc)P5XDL%~x@N2;Y-Wmnhzt0e}JV2Qru

37qB==G@B?i*%^Ze2JYlUsk0|RS8sDxV*;NG4-ZbXP{FE*b$ za&MQ5f8nBkX;t|?prsjEx>xwG!SK%)xsn(zk)=z`|3zAaErH3>Y1WbS9v%$&s$$NL zI6s%%!ViSvM=7BIuyfd=Egmgv{+3+%*$HL-{H47^opHRem7N2o(tXkWtH(%ZvbYVD zsMq*6KdPB--q+UqCx(0a{Y}YWQbXU9B%lXVNb;jvG)$g>(W2oIl}}W`etk~SXM(#! z3rbhTwu_akK6YGd=~k!Jn558Yz3T5I%|FQ%v#5P0sUsTIlG>vOnYA zwZDdVW8iWd{Kfac6p}y^d#Me6^Af5$+>dES6qJ>5y$@4-CoS;XD>v>~WB^rG9h)X6 z30Y#0iAzVTKHBind1>#?BACCsW%_tE_v}8n*)}2e#rmds4uyEgPABG$-n$;%h%P}*x@V&0!}48lw6Rr z)5Kv1AXK0UONE*CVhczNw@VU6&r%$g&~~f?cZe$FsY={aras{v1SDaTzp1} z(R@O{r;muHRns@G+CESHYHr;8A9vA%5;ZZ{ zT%J*Y1CC(x{!Z0ntZ~Efp3Q*n6`I~EuN6b3=0@?;e(i3#SNgvKIO!M9j6iyy8{a{a ze|g|Eg+V@_Ksbo#~rLQ2NRmcqsD7BsACD#+i9(T$Qgnhg@#Y zt9$+Y_~YlLe&3$g&;73JcG$J4^A`Q|F zf`BN}jiQ8pf$yOKiU5^)JVUwdTXKXV3Xr%V>5ncUEQ0ArOFr zvm6NATV4|%D<*wtWvx3p34Sfe@dZxx6cIIVgX3Gctd~myRv0lk&d#?%HK|xa$vWS6~1< z+-dTm_i%Ozc~bkc{9rYFc#P`UOC1;Qz|tTSS4`u{Jl6UOv~}|Jse_c&k6P2$%czz< z1o+6fj@JEGNQ3yd(F`9JuS#~ zQ}&XytU|O=$bCHb+kW>rS3ZS4;*5U;#sD!so~87^;mx=f2yp#5kW6N?`xRMP0wLML zFb?TJtT4pkacWw48f)?{BE8-kwZ>#q12Z)d(N6!DI{Z`mzhhN;NL`HBfml?$VX)+$ zLMe**4pc%Fq@AmGtaK#S7ZDjsXq9hjpfJ-@H@%na2#UeU>I14JTugKaSv7fmLsZQs zkhKuV7iz^j{9|^$tl^J#J{TFByNoEZKhypKsB?Fke5j*HnAwt`73MgYcL<9k>mYOo zw|USYO|(hsLe0G+q*$I#oA{txS%?o67*+LYSoE$CDtIja?*ZJu=ARIL*$BdC=laOW zRsep>r~CWV6t8>*T)*aHn&-BOfsZ8`R0x$^iejqHb+do}GGTo7pC4Uderx^b^%wa6 zY4cm=)8@DCKg(DdM*M~*q|3j>9KM#iq7uU+)E_h`ef5Y}T31Kqe`A7ne#`&AZ-44f zd9fhtW|H&OiJ^H)IimWF*kT@kwkLiM#th(CR@9;&$HTtG(L$=uZCV_4gTZ^EXBr)U zXLir{i}RiJVcp!_KJpxp1+R~gXoqY__NPP_QHL3uM7rGT+FOn8PeeC1XDm-7fINbv z7E!@)KJ`-Ua%<8JwEdcD-0}z-a>Hes+C0Cgs-4-(@CXMZxne-wh129i?GYk}qOhL8 zF@Qb7Me|A=$PGZ6*OlLIEf*Y+yaPQy`s1>GR{W%UbqpBW;1HP>onHH zt^8LfTCsFAwoFFI7v4IDqUPrAK4N3)9LOnEeV54PiPPM@l_5E&JY6=v1$={Y!%n+} zQr9fOhPyxpeAkPi*v)J*N|urVxwtzTG@0JhJ)SW%p%o>`bHzEr@G=beurKfxP+@qQ ze5egLPLzG|!w(Oz0e`_CDw@V8@LDU zAC0=@kSWuW%uRc5DVS*dlTq+Sw=`sp#vT;0YsBm}a8zl@X-u<8ev2j7P{mTA8R7Pz zoSMJb?%x*b$G&}W3ta#?T@nh)8UPFBt|8i2F~5PpQ>!BE?jvaV6hoKpXPAM^_^U;u z7S|J`zbsU?K(d_xb5nup^KmPk`f>K^K060Uz=-f?+|na?9pj(2*W%hnpQ6uedQXVi z3QRH9jFKJzJ`KhzuU<9DzG|V6RZh^nK3L?vGfXqls+@1)M+B$7S8`jffz|3l)Brgc~;j!gF|k z(~R$DLx4Gah>?q&hOAWG6b&W!*PmF)JoFn!E?}EAhiok%np?Cs^Rk@BOi^*sX-Y-c zvrxLeVmjVA^r4_Z>bd^q-!|*BO5FnaPG^X!3yJ{q-E*UunhS)C0?xv&pW>LY#?f)M z=B=d9uVCBO6(B~29Rbwpcf^o zgR@f}$NA`8Y>eFM`ZExrfg#gR{GzmC98~G%sDZc0m37JN8;FHrZbrsYUHv0qqwRvM z9V{$o^5pP37N4>+OvRh0(eI3`Ne3!g>I} zXf3t5F%~{Wz!FRnf1a}n8YV-YRxRFU*J0#YwrJTj+yBdG?S3>V*@<5(eAJ>JE#v)q zd~i@`4U{QIe!X#+^ftQpM$FzCI|zOQ~*6t%zTeA@GC0_ZNp044EGBE;UC2&L}MY&qeXy$o^0T z9VP9h#ntb5r%W!k5W(ylI0B-Iy`Ov$$3?08VCMia+n@bdTiYtoR z9J%}R`g;_sHTtyP>wKWG;Qj}OB=ORN+B|{A^sqB6PN~bCb$p&(FTz9-vL{~yO?sI| zo{4-#iOQTa=oAo>Jtl9v>L@*8pWLw(G-jZmWj_zqq<5hcpnABt)U!=`zq?N8hFI`Q z6FJYltxIE5-}$6D4%*}zYY(m6)+8BpcTTw3(vG82bFk$uy7l#$tN+tx@X;v*$Tt5{ z1fdoKZNofA&-AcJ*X!GE$rFpFXN(?)?ioJAj~*OAN6|V!vUYK)XQSw%%{N@g1}G+t zK#)m3{-Uy@r4XLsb)f0hSL9xwi6!Cn!stRJ5+{E<`aKTtQw^d=Pn>{E=`2GbHN6fe z2>??(2a^2kJ{OfS2_PiK+RC$fCIKmxw`b~MMLsSZ1upj8EBg}i`OcCSu^PV2A48$` z1LoJ^Kakw$bSA$zzLV!fVVqn5Sr+T4_p!>xlZs$)W+!qZGm_zk4EuP>%5?HKh-I>0 z1&7UP$SS8v>G%Ei^-y&(qd*nT<^7}~EC8*&Ba7 z|M#C~R9v*{Bm9n-^2LqOCr^g&lrpmVERwzX&L;5cKh5=HK>UM?vk#H`2XU4Jqq6JVbOZy-Y#fpk)QX&xP4#m(3G6;uBS-Q8a2CT(Yw7Q~ozAYGdq8n@1t>@ui%0~!;c3c^7 zHkk~Pi)S;|tg*gbLqF1X*VQ#C%2VdGVEvKR0K{ATRE!ex&Etom>}c|jF@juJ%4%`F zIgnko5!e`%GpDyNgEzR4WO<+-z-jWK=B-rAgcRW!ESNWB)_mhB`KTey;=%N(p&nU7-DP!tKOG(gdPZBb7xpp<3w;& zR|4{S=Rl(WFxt)`

A~hScnh_pQS+*e*c`KHCfw57C`*)JT(NYVadN?k#Xw{o(a4 z+ED3+%x1=?7!#YEn5sN8lb}zYyZBk;LJmww)yhkP@N!+44c>pSNgBcP<85J?1Qcvu zG3V+B@_^?+LO^EsC{HFG2#Ge@sA8hbXSRs#fC=>lco6aGW9vqtTd-BI-w?RVay9;7 z^P)N82gq#7(eJ@Fi3WN`-jVxi1dnXF{Y7Jh(E}ln<>lSaIBy>;{D)1$NTUSz?5DpT z7<}P~jClL%bs)cZ4kV%5{e-TnSwKkMGQUJebut)n&oh561-kw=PXBFP{`XzmYr``? zGz9VX|6ucC@8SfQY}8eLk2LY2s1v~sS$)WfjF&SYlp`j(C*f{t+bc6`Bz+kk5=nLa zHIRoqO+M5f@)=ukuTT0f-Y?qV6N0Qebg1MEsD&hSMDWQf4h-=>`C?YBazzrEeeg@O zS#2a%{-cFPjoTcu+Gy@T-~CxUB(mJ#O&||>4kWb^mP<@U=0HfS2S^DTLekTgN(iNU zD;Vk5pK66~k*kvz55w|%JFd3;p@kQFjxQYKs;{Z*p<=jO*OljZW6r8kdz?~95mPYR znICSMtv$kDduR?-qhV1JpgDS~REz{8$aq79^2htLnH*V(!UqrNb*kQv;*NV_;M&>p zmAxf=-)RL88=if^H(3MNsiU1re=mOv`Z1pJoA>j`lS_*F%h#)=_8;6O-AdEApK=*? ziXvJ%1Qa(;lMl6>Ha%9!u8UmY>W^|b^-qUbA)7f_ z;YzIQLo*-l^Bb&XCETlch{NSwbQ#Kr#%MJR6kJb}54G}jIX%3|;t>s2z9N(MM@z3P zQjwEQJ8ntxiZQVcA%I9*8}7V4o+P01Tj*opZ~SnO+eEC7XwLrC$9YF(HC!KPbQ{~e zUnE1Vkr8<7R!mqv5^YV#R8S*zT0KYG;XQpeh&m_q@E@p`k6*~s^cRe zjMre|L|cmBdrmrU_a@YbC}1*6WOABRg1C>ERG+MnV%iFZJLk7|_T4jRq+J_LBJL1V zOx__(=>Z}x;gb+COnXIu*uVlcc_Go&u@;ptcrQMrd8glN#Bt6lxD4Xd<$_$mxFOEG zPm>QlaWAJ$>=Y*4zyL1xHdKH~$FwVb5vrzY_9{%0jD?8>y-aN|GlHQhJZ_k?u+`7E zd(b5yHUPt*i%UJ*q$4t+hbh`A0*N*pQDj$~Lh9|wGY3$?T0xA!yQtDt0$HMjq#C0p zm<${I1}5`YD^8Sq+FGGNy_C4PmR|GwYugqPMV~0;|@@q8_5`wwQ=pvS3J5`W^iqF10>TcaX7gbQq67 z8;>pl{Z3pg9&)NIS$@)Pyd_6lx)wM?1_%OPc>~)^6GzGzR3(!uibno?Mt*6!`cKmj zrWsj3%D9DI25%U6bfZ9-+iCKl=IyQg^l^>>Gnh99T+Q7=8|hxIVUO;dXPCr&decE5 zlMQmY}8MD|4Q8~IY`z49>sBn)a@B9z zboqxXCk-OmqtLZtmun@cN(PfXUViS*vU&ea;>n(O7b6st$V7l)__kaBQ_+`)d*jbFP2?I==>g|G3^J(btnrIwsm(kzS}mR>t_G{Wy1KJKR>#_`7_%;um3;zKdADm z?pyBz3R~|Z9TJ*gPLY_5ed<@Y?+zN#xp!6WU?-h+KJ9;Fg7*9wQ{ex;|7kyEh`jLF z0^pI@_mO3jhhn*Ix!f03nJGjIbTrIdX-h@^(MxLKW1cLI_}@mmYiZVU>*7W0GZlc7 z73W29Yo-za#l4(+T@NVSB!x$Y_&Mb6xZ0#2)u^-F^a-9n*`L7ZANkw!4Rw}hI0>x! zS|I%}*&=ylQi`dN1G3{qb>eR@MRhgQy-6F!TR|QM|D35Ka#yj1pQt%pKM5#bX))tP zw?=*&kbLdEVID@TGk#%MU~%*8kzf+NxpeJ*82_gI7Xh2QEi%moqj>atW-dwX^&DCVq#bN z<}leZ5N{*9vV#gcdQDWx3Rh5?i(1sZwpl4T9z+JH!6&)W}CEu%!p+}t5jLzM^s-(azMkPaSd5hG-7H(YwpI}Iwoo9%GFKn zZUI$pT!tU~iur)@Jf)cxN&4}1{sZG_&O$hR{k-XiX-hcE37k!B&u`A?Y+Xj4r^zku zE&-6|ohBdpJa5KGfxybC1x(yU$BI3W^SoME&3-p~68)FuI-cAgdYbI6SjRP{wyNZp zQLZa*g+Bb3w=>)PXaSI6oh8rn09cqkFN(R7b^sI}g7qT%igbH!_ZED2>%KOk?roXH zh|-tr1X~91N0npz?d>i)O`L&DqK?!F>XRd7&1q%E+uEa;T(CjbYOaVI75rD4ZmMss z-~1PehDwf|0M7a>BxVEjInRqiFa~^1W?29A%G==Sbu7G?=e;uixg8^l9iT>AwC3fm z*++W}&+a&9IWto8@=LsXjm0797lbev=BoZTiM|a2rH8m{O?&NXvr+h%x6AmFgsm6dv$Zj|fZCfZPTvGf%*Kq7d6YulQd)V|Nbj^xDTIKRKvr z7Wbd@S0UIR_*e$wYKxp7ybKf8VkFxM1Tf)KDKDQJ!9aO6$BRdhQPcpm4M< zf30aP6j1r5isWio`F$D7Srugg&R%m&4GI>^Hp0J^=c1E_9?0sG(R459P94P$=VlYJ z9~oP`!_GhT`us8LwCq)#?^)#iXXkT|uOg#4pX^;2kRqIAgI1gT&=c&6Au94YkYwsv zMJ5$%0wIwo+2l4>n5RO({yx3PkNJuNJ4I(m?{QiD%#HA7DRYqVcX>XTE zq4^R$^R{ZvuZp8Y@Bjro*`MN#U&DS6|7G^OC+8UUhtg%T%FwN5Sz=rg4G$}zXzGP) zGy18tJT;wMF!RHX|A{+I2u;$=e6)IYui?s%;(^OBMIGFUeHZ`+Pm>RQG+oaXUiA{S z0DpxS?V89z{_=4dHc8$|!m){F4O?f61}4DiOS_>sDU2@_`PGt#qTv3vtX6D&Mlu8*iXJ@69?U84-@C{6nlBY z*&~{Xl|~Wm4>iP1ntbSBSEgZqTy;BWBL``w|A{3-*kPdEijJq`~ihS!v+ ztZpECE@64+qa7?fpk!Dy1*hiw&5gCxG)(uOsz`b_%xA-`oVwMpA6~=!Aenn`8Obo# zk5*lvX7OqAp-zTBM)ri<5+DR?A^+MlIgjR$Ec?z5H4xSe;m*yy=kgs% zW;maViz3PFyxG_?n*%U3`+N*^%R_-qTv+lUe#f9qt#W(MV$(2R@oni@e!v4>Wp|f3 zkMazmduq_lAzlEiyI)-D+2#-z-A%NDY^X8n*7*{>SquajRF=lmBxX_XDkn*(851?yv~7y_jb1XY#1UC64`bJk`{xVjcq|z zWKywvlL87t$?~~!n@>-UUZi&QC;efl7wvNboE9);ggKnjxe3pRnzpv!HH~qV2GJ*h zuEEC9o>*~oBSGgko5#lVhHbS(%)h3V7v843x|%f!G^scT642J6=1O-n5R%)Di5 zE3V@i(4B~*-R`W!3-Pwc#y444M9$e?CzB=%S-l-dhOXAbe(&ucx|w5vrLRCN$|66t zM{1x#^CP+w6AR>Qvx5+mH$*MnZ0TuI9#C1JvR_LkR`~j6Ds9@$x8t)w2SAPbw?z^j zM<3D(YY04C8K!FKtvlGlrQfKUsmL)vT(}QTM!f-wptEGtn*iOr)5QMegCUk)MIx{F zimWG)tnL6x+v5@MO$b zDnWM|g#@f*wr5iKpIDc-B?iv-$oD7B94*+sTskw!a{J)wH-~#1JiU%p3$2SVP`I|+tLncFQbBZ$=Rg8}1xK<$ z$}^xl@g%cNMkccA-Rm)R)e6aZ7QCi;->l|?vMY^ymv8o3(?;cjuR-ZTBhMkct~cGBCtwia3z2EEHYv1W%c zIXy6pK+S?Wj%uA@C)hO7yR{n2Ax37lIem`us(OQpWsOqnE z8_fLlQwXcBD)G1Z{u5BVM-!n4D1rdn0p~{1r3MgC6s_s-yVGP0#fe^i+96|4Rc9g? zlE}L1DBxQC;0+3z=wAWlqLl~0hB3<6W=v_G1XVGH5t{o^<}yzPVm$U&LL5_asmC!N zx@!KL!(FpdJwBKVH&5$H0mc{SMe*1x0RRQhbJj#SkKll?Hkb6iU2+nUZ{;HgB#qaV zSTjqD8bUq)Zl10ozpZI^=ff57-iR9!$Z)iqD~AKsP<$aUf}6kNZH{ktjS6!Pp4uv- zC6;!#g;)BCj=`offCW?L6ucV>X7sLnWpUTb?As_Nlj2LFHzZd21xI#xJ}OO@VZe6= zHi7^gz-i*4wqIl{deFm1!@thcUNj-3hFq(r%$v3b%)V0y9Sb;_^t(55H+T>I+d*&N zdShV)ec)o9w9k_Mm+&=CV6c9zL#cNGyU)QLtZd2|^CJ*r8Z> zcg10|enx{waWaTDTG)hKI>S6J-s$P!9wHgO!~Ng3`v0B@vP=U-tThH`lAasI_(y

L&(44g+ug+v^|E^M7 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 diff --git a/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/goerliShadowForkBlock.13249/streamed.snappy b/packages/beacon-node/test/unit-mainnet/network/reqresp/encodingStrategies/sszSnappy/goerliShadowForkBlock.13249/streamed.snappy deleted file mode 100644 index 4589528f61847ebb6283d8cf695275fc15b58b61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52357 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­+MBBXUw~ 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 nmnTM#&o(cMU?A { - id: string; - type: RequestOrResponseType; - bytes: Buffer; - streamedBody: Buffer; - body?: T; -} - -/** - * A real big bellatrix block from goerli-shadow-fork-2 devnet, which is expected to be - * encoded in multiple chunks. - */ - -export const goerliShadowForkBlock13249: ISszSnappyTestBlockData = { - id: "goerli-shadow-fork-block-13249", - type: ssz.bellatrix.SignedBeaconBlock, - bytes: fs.readFileSync(path.join(__dirname, "/goerliShadowForkBlock.13249/serialized.ssz")), - streamedBody: fs.readFileSync(path.join(__dirname, "/goerliShadowForkBlock.13249/streamed.snappy")), -}; diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts deleted file mode 100644 index d949423e9c8b..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/request.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {expect} from "chai"; -import all from "it-all"; -import {pipe} from "it-pipe"; -import {Uint8ArrayList} from "uint8arraylist"; -import {ReqRespMethod, Encoding, RequestBody} from "../../../../../src/network/reqresp/types.js"; -import {SszSnappyErrorCode} from "../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; -import {requestEncode} from "../../../../../src/network/reqresp/encoders/requestEncode.js"; -import {requestDecode} from "../../../../../src/network/reqresp/encoders/requestDecode.js"; -import {sszSnappyPing} from "../encodingStrategies/sszSnappy/testData.js"; -import {arrToSource, expectEqualByteChunks} from "../utils.js"; - -describe("network / reqresp / encoders / request - Success and error cases", () => { - const testCases: { - id: string; - method: ReqRespMethod; - encoding: Encoding; - chunks: Uint8ArrayList[]; - // decode - errorDecode?: string; - // encode - requestBody?: RequestBody; - }[] = [ - { - id: "Bad body", - method: ReqRespMethod.Status, - encoding: Encoding.SSZ_SNAPPY, - errorDecode: SszSnappyErrorCode.UNDER_SSZ_MIN_SIZE, - chunks: [new Uint8ArrayList(Buffer.from("4"))], - }, - { - id: "No body on Metadata - Ok", - method: ReqRespMethod.Metadata, - encoding: Encoding.SSZ_SNAPPY, - requestBody: null, - chunks: [], - }, - { - id: "No body on Status - Error", - method: ReqRespMethod.Status, - encoding: Encoding.SSZ_SNAPPY, - errorDecode: SszSnappyErrorCode.SOURCE_ABORTED, - chunks: [], - }, - { - id: "Regular request", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - requestBody: sszSnappyPing.body, - chunks: sszSnappyPing.chunks, - }, - ]; - - for (const {id, method, encoding, errorDecode, requestBody, chunks} of testCases) { - it(`${id} - requestDecode`, async () => { - const promise = pipe(arrToSource(chunks), requestDecode({method, encoding})); - if (errorDecode) { - await expect(promise).to.be.rejectedWith(errorDecode); - } else { - await promise; - } - }); - - if (requestBody !== undefined) { - it(`${id} - requestEncode`, async () => { - const encodedChunks = await pipe(requestEncode({method, encoding}, requestBody), all); - expectEqualByteChunks( - encodedChunks, - chunks.map((c) => c.subarray()) - ); - }); - } - } -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts deleted file mode 100644 index eda4777369b2..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/requestTypes.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {expect} from "chai"; -import {pipe} from "it-pipe"; -import {phase0} from "@lodestar/types"; -import { - ReqRespMethod, - Encoding, - getRequestSzzTypeByMethod, - RequestBody, -} from "../../../../../src/network/reqresp/types.js"; -import {requestEncode} from "../../../../../src/network/reqresp/encoders/requestEncode.js"; -import {requestDecode} from "../../../../../src/network/reqresp/encoders/requestDecode.js"; -import {isEqualSszType} from "../../../../utils/ssz.js"; -import {createStatus, generateRoots} from "../utils.js"; - -// Ensure the types from all methods are supported properly -describe("network / reqresp / encoders / request - types", () => { - interface IRequestTypes { - [ReqRespMethod.Status]: phase0.Status; - [ReqRespMethod.Goodbye]: phase0.Goodbye; - [ReqRespMethod.Ping]: phase0.Ping; - [ReqRespMethod.Metadata]: null; - [ReqRespMethod.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest; - [ReqRespMethod.BeaconBlocksByRoot]: phase0.BeaconBlocksByRootRequest; - } - - const testCases: {[P in keyof IRequestTypes]: IRequestTypes[P][]} = { - [ReqRespMethod.Status]: [createStatus()], - [ReqRespMethod.Goodbye]: [BigInt(0), BigInt(1)], - [ReqRespMethod.Ping]: [BigInt(0), BigInt(1)], - [ReqRespMethod.Metadata]: [], - [ReqRespMethod.BeaconBlocksByRange]: [{startSlot: 10, count: 20, step: 1}], - [ReqRespMethod.BeaconBlocksByRoot]: [generateRoots(4, 0xda)], - }; - - const encodings: Encoding[] = [Encoding.SSZ_SNAPPY]; - - for (const encoding of encodings) { - for (const [_method, _requests] of Object.entries(testCases)) { - // Cast to more generic types, type by index is useful only at declaration of `testCases` - const method = _method as keyof typeof testCases; - const requests = _requests as RequestBody[]; - - for (const [i, request] of requests.entries()) { - it(`${encoding} ${method} - req ${i}`, async () => { - const returnedRequest = await pipe( - requestEncode({method, encoding}, request), - requestDecode({method, encoding}) - ); - - const type = getRequestSzzTypeByMethod(method); - if (!type) throw Error("no type"); - - expect(isEqualSszType(type, returnedRequest, request)).to.equal( - true, - "decoded request does not match encoded request" - ); - }); - } - } - } -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts deleted file mode 100644 index 303b48807c0b..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/response.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import {pipe} from "it-pipe"; -import all from "it-all"; -import {Uint8ArrayList} from "uint8arraylist"; -import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {chainConfig} from "@lodestar/config/default"; -import {createIBeaconConfig} from "@lodestar/config"; -import {fromHex, LodestarError} from "@lodestar/utils"; -import {allForks} from "@lodestar/types"; -import { - ReqRespMethod, - Version, - Encoding, - Protocol, - IncomingResponseBody, - OutgoingResponseBody, -} from "../../../../../src/network/reqresp/types.js"; -import {getResponseSzzTypeByMethod} from "../../../../../src/network/reqresp/types.js"; -import { - SszSnappyError, - SszSnappyErrorCode, -} from "../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; -import {responseDecode} from "../../../../../src/network/reqresp/encoders/responseDecode.js"; -import { - getForkNameFromResponseBody, - responseEncodeError, - responseEncodeSuccess, -} from "../../../../../src/network/reqresp/encoders/responseEncode.js"; -import {ResponseError} from "../../../../../src/network/reqresp/response/index.js"; -import {RespStatus, ZERO_HASH} from "../../../../../src/constants/index.js"; -import {expectIsEqualSszTypeArr} from "../../../../utils/ssz.js"; -import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; -import {arrToSource, expectEqualByteChunks} from "../utils.js"; -import { - sszSnappyPing, - sszSnappySignedBeaconBlockPhase0, - sszSnappySignedBeaconBlockAltair, -} from "../encodingStrategies/sszSnappy/testData.js"; -import {blocksToReqRespBlockResponses} from "../../../../utils/block.js"; - -type ResponseChunk = - | {status: RespStatus.SUCCESS; body: IncomingResponseBody} - | {status: Exclude; errorMessage: string}; - -describe("network / reqresp / encoders / response - Success and error cases", () => { - const methodDefault = ReqRespMethod.Status; - const encodingDefault = Encoding.SSZ_SNAPPY; - - // Set the altair fork to happen between the two precomputed SSZ snappy blocks - const slotBlockPhase0 = sszSnappySignedBeaconBlockPhase0.body.message.slot; - const slotBlockAltair = sszSnappySignedBeaconBlockAltair.body.message.slot; - if (slotBlockAltair - slotBlockPhase0 < SLOTS_PER_EPOCH) { - throw Error("phase0 block slot must be an epoch apart from altair block slot"); - } - const ALTAIR_FORK_EPOCH = Math.floor(slotBlockAltair / SLOTS_PER_EPOCH); - // eslint-disable-next-line @typescript-eslint/naming-convention - const config = createIBeaconConfig({...chainConfig, ALTAIR_FORK_EPOCH}, ZERO_HASH); - - const testCases: { - id: string; - method?: ReqRespMethod; - version?: Version; - encoding?: Encoding; - chunks?: Uint8ArrayList[]; - responseChunks?: ResponseChunk[]; - // decode only - decodeError?: LodestarError; - // encode only - encodeError?: LodestarError; - skipEncoding?: boolean; - }[] = [ - { - id: "No chunks should be ok", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [], - chunks: [], - }, - { - id: "Empty response chunk - Error", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - decodeError: new SszSnappyError({code: SszSnappyErrorCode.SOURCE_ABORTED}), - chunks: [new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS]))], - // Not possible to encode this invalid case - }, - { - id: "Single chunk - wrong body SSZ type", - method: ReqRespMethod.Status, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [{status: RespStatus.SUCCESS, body: BigInt(1)}], - chunks: [new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), ...sszSnappyPing.chunks], - // decode will throw since Ping's Uint64 is smaller than Status min size - decodeError: new SszSnappyError({code: SszSnappyErrorCode.UNDER_SSZ_MIN_SIZE, minSize: 84, sszDataLength: 8}), - // encode will throw when trying to serialize a Uint64 - encodeError: new SszSnappyError({ - code: SszSnappyErrorCode.SERIALIZE_ERROR, - serializeError: Error("Cannot convert undefined or null to object"), - }), - }, - { - id: "block v1 without ", - method: ReqRespMethod.BeaconBlocksByRange, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [{status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockPhase0.body}], - chunks: [ - // - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - // | - ...sszSnappySignedBeaconBlockPhase0.chunks, - ], - }, - { - id: "block v2 with phase0", - method: ReqRespMethod.BeaconBlocksByRange, - version: Version.V2, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [{status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockPhase0.body}], - chunks: [ - // - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - // - new Uint8ArrayList(config.forkName2ForkDigest(ForkName.phase0)), - // | - ...sszSnappySignedBeaconBlockPhase0.chunks, - ], - }, - { - id: "block v2 with altair", - method: ReqRespMethod.BeaconBlocksByRange, - version: Version.V2, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [{status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockAltair.body}], - chunks: [ - // - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - // - new Uint8ArrayList(config.forkName2ForkDigest(ForkName.altair)), - // | - ...sszSnappySignedBeaconBlockAltair.chunks, - ], - }, - - { - id: "Multiple chunks with success", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [ - {status: RespStatus.SUCCESS, body: sszSnappyPing.body}, - {status: RespStatus.SUCCESS, body: sszSnappyPing.body}, - ], - chunks: [ - // Chunk 0 - success - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - ...sszSnappyPing.chunks, - // Chunk 1 - success - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - ...sszSnappyPing.chunks, - ], - }, - { - id: "Multiple chunks with final error, should error", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - decodeError: new ResponseError(RespStatus.SERVER_ERROR, ""), - responseChunks: [ - {status: RespStatus.SUCCESS, body: sszSnappyPing.body}, - {status: RespStatus.SUCCESS, body: sszSnappyPing.body}, - {status: RespStatus.SERVER_ERROR, errorMessage: ""}, - ], - chunks: [ - // Chunk 0 - success - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - ...sszSnappyPing.chunks, - // Chunk 1 - success - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - ...sszSnappyPing.chunks, - // Chunk 2 - error - new Uint8ArrayList(Buffer.from([RespStatus.SERVER_ERROR])), - ], - }, - { - id: "Decode successfully response_chunk as a single Uint8ArrayList", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [ - {status: RespStatus.SUCCESS, body: BigInt(1)}, - {status: RespStatus.SUCCESS, body: BigInt(1)}, - ], - chunks: [ - // success, Ping payload = BigInt(1) - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS]), ...sszSnappyPing.chunks), - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS]), ...sszSnappyPing.chunks), - ], - // It's not able to produce a single concatenated chunk with our encoder - skipEncoding: true, - }, - - { - id: "Decode successfully response_chunk as a single concated chunk", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [ - {status: RespStatus.SUCCESS, body: BigInt(1)}, - {status: RespStatus.SUCCESS, body: BigInt(1)}, - ], - chunks: [ - // success, Ping payload = BigInt(1) - new Uint8ArrayList( - Buffer.concat([Buffer.from([RespStatus.SUCCESS]), ...sszSnappyPing.chunks.map((c) => c.subarray())]) - ), - new Uint8ArrayList( - Buffer.concat([Buffer.from([RespStatus.SUCCESS]), ...sszSnappyPing.chunks.map((c) => c.subarray())]) - ), - ], - // It's not able to produce a single concatenated chunk with our encoder - skipEncoding: true, - }, - - { - id: "Decode blocks v2 through a fork with multiple types", - method: ReqRespMethod.BeaconBlocksByRange, - version: Version.V2, - encoding: Encoding.SSZ_SNAPPY, - responseChunks: [ - {status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockPhase0.body}, - {status: RespStatus.SUCCESS, body: sszSnappySignedBeaconBlockAltair.body}, - ], - chunks: [ - // Chunk 0 - success block in phase0 with context bytes - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - new Uint8ArrayList(config.forkName2ForkDigest(ForkName.phase0)), - ...sszSnappySignedBeaconBlockPhase0.chunks, - // Chunk 1 - success block in altair with context bytes - new Uint8ArrayList(Buffer.from([RespStatus.SUCCESS])), - new Uint8ArrayList(config.forkName2ForkDigest(ForkName.altair)), - ...sszSnappySignedBeaconBlockAltair.chunks, - ], - }, - - // Errored requests - { - id: "INVALID_REQUEST - no error message", - decodeError: new ResponseError(RespStatus.INVALID_REQUEST, ""), - chunks: [new Uint8ArrayList(Buffer.from([RespStatus.INVALID_REQUEST]))], - responseChunks: [{status: RespStatus.INVALID_REQUEST, errorMessage: ""}], - }, - { - id: "SERVER_ERROR - no error message", - decodeError: new ResponseError(RespStatus.SERVER_ERROR, ""), - chunks: [new Uint8ArrayList(Buffer.from([RespStatus.SERVER_ERROR]))], - responseChunks: [{status: RespStatus.SERVER_ERROR, errorMessage: ""}], - }, - { - id: "SERVER_ERROR - with error message", - decodeError: new ResponseError(RespStatus.SERVER_ERROR, "sNaPpYIzTEST_ERROR"), - chunks: [ - new Uint8ArrayList(Buffer.from([RespStatus.SERVER_ERROR])), - new Uint8ArrayList(fromHexBuf("0x0a")), - new Uint8ArrayList(fromHexBuf("0xff060000734e61507059010e000049b97aaf544553545f4552524f52")), - ], - responseChunks: [{status: RespStatus.SERVER_ERROR, errorMessage: "TEST_ERROR"}], - }, - // This last two error cases are not possible to encode since are invalid. Test decoding only - { - id: "SERVER_ERROR - Slice long error message", - decodeError: new ResponseError(RespStatus.SERVER_ERROR, "TEST_ERROR".padEnd(512, "0").slice(0, 256)), - chunks: [ - new Uint8ArrayList(Buffer.from([RespStatus.SERVER_ERROR])), - new Uint8ArrayList(Buffer.from("TEST_ERROR".padEnd(512, "0"))), - ], - }, - { - id: "SERVER_ERROR - Remove non-ascii characters from error message", - decodeError: new ResponseError(RespStatus.SERVER_ERROR, "TEST_ERROR"), - chunks: [ - new Uint8ArrayList(Buffer.from([RespStatus.SERVER_ERROR])), - new Uint8ArrayList(Buffer.from("TEST_ERROR\u03A9")), - ], - }, - ]; - - async function* responseEncode(responseChunks: ResponseChunk[], protocol: Protocol): AsyncIterable { - for (const chunk of responseChunks) { - if (chunk.status === RespStatus.SUCCESS) { - const lodestarResponseBodies = - protocol.method === ReqRespMethod.BeaconBlocksByRange || protocol.method === ReqRespMethod.BeaconBlocksByRoot - ? blocksToReqRespBlockResponses([chunk.body] as allForks.SignedBeaconBlock[], config) - : [chunk.body]; - yield* pipe( - arrToSource(lodestarResponseBodies as OutgoingResponseBody[]), - responseEncodeSuccess(config, protocol) - ); - } else { - yield* responseEncodeError(protocol, chunk.status, chunk.errorMessage); - } - } - } - - for (const testData of testCases) { - const { - id, - method = methodDefault, - version = Version.V1, - encoding = encodingDefault, - chunks, - responseChunks, - decodeError, - encodeError, - skipEncoding, - } = testData; - const protocol: Protocol = {method, version, encoding}; - - if (chunks) { - it(`${id} - responseDecode`, async () => { - const responseDecodePromise = pipe( - arrToSource(chunks), - // eslint-disable-next-line @typescript-eslint/no-empty-function - responseDecode(config, protocol, {onFirstHeader: () => {}, onFirstResponseChunk: () => {}}), - all - ); - - if (decodeError) { - await expectRejectedWithLodestarError(responseDecodePromise, decodeError); - } else if (responseChunks) { - const responses = await responseDecodePromise; - const typeArr = responses.map((body) => { - const forkName = getForkNameFromResponseBody(config, protocol, body); - return getResponseSzzTypeByMethod(protocol, forkName); - }); - expectIsEqualSszTypeArr(typeArr, responses, onlySuccessChunks(responseChunks), "Response chunks"); - } else { - throw Error("Bad testCase"); - } - }); - } - - if (responseChunks && !skipEncoding) { - it(`${id} - responseEncode`, async () => { - const resultPromise = all(responseEncode(responseChunks, protocol)); - - if (encodeError) { - await expectRejectedWithLodestarError(resultPromise, encodeError); - } else if (chunks) { - const encodedChunks = await resultPromise; - expectEqualByteChunks( - encodedChunks, - chunks.map((c) => c.subarray()) - ); - } else { - throw Error("Bad testCase"); - } - }); - } - } -}); - -function onlySuccessChunks(responseChunks: ResponseChunk[]): IncomingResponseBody[] { - const bodyArr: IncomingResponseBody[] = []; - for (const chunk of responseChunks) { - if (chunk.status === RespStatus.SUCCESS) bodyArr.push(chunk.body); - } - return bodyArr; -} - -function fromHexBuf(hex: string): Buffer { - return Buffer.from(fromHex(hex)); -} diff --git a/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts b/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts deleted file mode 100644 index 41b0ba10beef..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encoders/responseTypes.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {pipe} from "it-pipe"; -import all from "it-all"; -import {allForks, ssz} from "@lodestar/types"; -import {ForkName} from "@lodestar/params"; -import { - ReqRespMethod, - Version, - Encoding, - OutgoingResponseBody, - getResponseSzzTypeByMethod, - IncomingResponseBodyByMethod, -} from "../../../../../src/network/reqresp/types.js"; -import {responseDecode} from "../../../../../src/network/reqresp/encoders/responseDecode.js"; -import {responseEncodeSuccess} from "../../../../../src/network/reqresp/encoders/responseEncode.js"; -import {arrToSource, createStatus, generateEmptySignedBlocks} from "../utils.js"; -import {expectIsEqualSszTypeArr} from "../../../../utils/ssz.js"; -import {config} from "../../../../utils/config.js"; -import {blocksToReqRespBlockResponses} from "../../../../utils/block.js"; - -// Ensure the types from all methods are supported properly -describe("network / reqresp / encoders / responseTypes", () => { - const testCases: {[P in keyof IncomingResponseBodyByMethod]: IncomingResponseBodyByMethod[P][][]} = { - [ReqRespMethod.Status]: [[createStatus()]], - [ReqRespMethod.Goodbye]: [[BigInt(0)], [BigInt(1)]], - [ReqRespMethod.Ping]: [[BigInt(0)], [BigInt(1)]], - [ReqRespMethod.Metadata]: [], - [ReqRespMethod.BeaconBlocksByRange]: [generateEmptySignedBlocks(2)], - [ReqRespMethod.BeaconBlocksByRoot]: [generateEmptySignedBlocks(2)], - [ReqRespMethod.LightClientBootstrap]: [[ssz.altair.LightClientBootstrap.defaultValue()]], - [ReqRespMethod.LightClientUpdatesByRange]: [[ssz.altair.LightClientUpdate.defaultValue()]], - [ReqRespMethod.LightClientFinalityUpdate]: [[ssz.altair.LightClientFinalityUpdate.defaultValue()]], - [ReqRespMethod.LightClientOptimisticUpdate]: [[ssz.altair.LightClientOptimisticUpdate.defaultValue()]], - }; - - const encodings: Encoding[] = [Encoding.SSZ_SNAPPY]; - - // TODO: Test endcoding through a fork - const forkName = ForkName.phase0; - - for (const encoding of encodings) { - for (const [_method, responsesChunks] of Object.entries(testCases)) { - // Cast to more generic types, type by index is useful only at declaration of `testCases` - const method = _method as keyof typeof testCases; - // const responsesChunks = _responsesChunks as LodestarResponseBody[][]; - const lodestarResponseBodies = - _method === ReqRespMethod.BeaconBlocksByRange || _method === ReqRespMethod.BeaconBlocksByRoot - ? responsesChunks.map((chunk) => blocksToReqRespBlockResponses(chunk as allForks.SignedBeaconBlock[])) - : (responsesChunks as OutgoingResponseBody[][]); - - const versions = - method === ReqRespMethod.BeaconBlocksByRange || method === ReqRespMethod.BeaconBlocksByRoot - ? [Version.V1, Version.V2] - : [Version.V1]; - - for (const version of versions) { - for (const [i, responseChunks] of responsesChunks.entries()) { - it(`${encoding} v${version} ${method} - resp ${i}`, async function () { - const protocol = {method, version, encoding}; - const returnedResponses = await pipe( - arrToSource(lodestarResponseBodies[i]), - responseEncodeSuccess(config, protocol), - // eslint-disable-next-line @typescript-eslint/no-empty-function - responseDecode(config, protocol, {onFirstHeader: () => {}, onFirstResponseChunk: () => {}}), - all - ); - - const type = getResponseSzzTypeByMethod(protocol, forkName); - if (type === undefined) throw Error("no type"); - - expectIsEqualSszTypeArr(type, returnedResponses, responseChunks, "Response chunks"); - }); - } - } - } - } -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts b/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts deleted file mode 100644 index 9e72e4e29af0..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/decode.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {expect} from "chai"; -import varint from "varint"; -import {Uint8ArrayList} from "uint8arraylist"; -import {ssz} from "@lodestar/types"; -import {RequestOrResponseType} from "../../../../../../src/network/reqresp/types.js"; -import {BufferedSource} from "../../../../../../src/network/reqresp/utils/index.js"; -import { - SszSnappyErrorCode, - readSszSnappyPayload, -} from "../../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; -import {isEqualSszType} from "../../../../../utils/ssz.js"; -import {arrToSource} from "../../utils.js"; -import {sszSnappyPing, sszSnappyStatus, sszSnappySignedBeaconBlockPhase0} from "./testData.js"; - -describe("network / reqresp / sszSnappy / decode", () => { - describe("Test data vectors (generated in a previous version)", () => { - const testCases = [sszSnappyPing, sszSnappyStatus, sszSnappySignedBeaconBlockPhase0]; - - for (const {id, type, body, chunks} of testCases) { - it(id, async () => { - const bufferedSource = new BufferedSource(arrToSource(chunks)); - const bodyResult = await readSszSnappyPayload(bufferedSource, type); - expect(isEqualSszType(type, bodyResult, body)).to.equal(true, "Wrong decoded body"); - }); - } - }); - - describe("Error cases", () => { - const testCases: { - id: string; - type: RequestOrResponseType; - error: SszSnappyErrorCode; - chunks: Buffer[]; - }[] = [ - { - id: "if it takes more than 10 bytes for varint", - type: ssz.phase0.Status, - error: SszSnappyErrorCode.INVALID_VARINT_BYTES_COUNT, - // Used varint@5.0.2 to generated this hex payload because of https://github.com/chrisdickinson/varint/pull/20 - chunks: [Buffer.from("80808080808080808080808010", "hex")], - }, - { - id: "if failed ssz size bound validation", - type: ssz.phase0.Status, - error: SszSnappyErrorCode.UNDER_SSZ_MIN_SIZE, - chunks: [Buffer.alloc(12, 0)], - }, - { - id: "if it read more than maxEncodedLen", - type: ssz.phase0.Ping, - error: SszSnappyErrorCode.TOO_MUCH_BYTES_READ, - chunks: [Buffer.from(varint.encode(ssz.phase0.Ping.minSize)), Buffer.alloc(100)], - }, - { - id: "if failed ssz snappy input malformed", - type: ssz.phase0.Status, - error: SszSnappyErrorCode.DECOMPRESSOR_ERROR, - chunks: [Buffer.from(varint.encode(ssz.phase0.Status.minSize)), Buffer.from("wrong snappy data")], - }, - ]; - - for (const {id, type, error, chunks} of testCases) { - it(id, async () => { - const bufferedSource = new BufferedSource(arrToSource([new Uint8ArrayList(...chunks)])); - await expect(readSszSnappyPayload(bufferedSource, type)).to.be.rejectedWith(error); - }); - } - }); -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts b/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts deleted file mode 100644 index fc364b526c41..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/encode.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import all from "it-all"; -import {pipe} from "it-pipe"; -import {allForks, ssz} from "@lodestar/types"; -import {LodestarError} from "@lodestar/utils"; -import { - reqRespBlockResponseSerializer, - RequestOrIncomingResponseBody, - RequestOrResponseType, -} from "../../../../../../src/network/reqresp/types.js"; -import { - SszSnappyError, - SszSnappyErrorCode, - writeSszSnappyPayload, -} from "../../../../../../src/network/reqresp/encodingStrategies/sszSnappy/index.js"; -import {expectRejectedWithLodestarError} from "../../../../../utils/errors.js"; -import {expectEqualByteChunks} from "../../utils.js"; -import {blocksToReqRespBlockResponses} from "../../../../../utils/block.js"; -import {RequestOrOutgoingResponseBody} from "../../../../../../src/network/reqresp/types.js"; -import {sszSnappyPing, sszSnappyStatus, sszSnappySignedBeaconBlockPhase0} from "./testData.js"; - -describe("network / reqresp / sszSnappy / encode", () => { - describe("Test data vectors (generated in a previous version)", () => { - const testCases = [sszSnappyPing, sszSnappyStatus, sszSnappySignedBeaconBlockPhase0]; - - for (const testCase of testCases) { - const {id, type, chunks} = testCase; - it(id, async () => { - const body = - type === ssz.phase0.SignedBeaconBlock - ? blocksToReqRespBlockResponses([testCase.body] as allForks.SignedBeaconBlock[])[0] - : testCase.body; - const encodedChunks = await pipe( - writeSszSnappyPayload( - body as RequestOrOutgoingResponseBody, - type === ssz.phase0.SignedBeaconBlock ? reqRespBlockResponseSerializer : type - ), - all - ); - expectEqualByteChunks( - encodedChunks, - chunks.map((c) => c.subarray()) - ); - }); - } - }); - - describe("Error cases", () => { - const testCases: { - id: string; - type: RequestOrResponseType; - body: RequestOrIncomingResponseBody; - error: LodestarError; - }[] = [ - { - id: "Bad body", - type: ssz.phase0.Status, - body: BigInt(1), - error: new SszSnappyError({ - code: SszSnappyErrorCode.SERIALIZE_ERROR, - serializeError: new TypeError("Cannot convert undefined or null to object"), - }), - }, - ]; - - for (const {id, type, body, error} of testCases) { - it(id, async () => { - await expectRejectedWithLodestarError( - pipe(writeSszSnappyPayload(body as RequestOrOutgoingResponseBody, type), all), - error - ); - }); - } - }); -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/snappy-frames/uncompress.test.ts b/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/snappy-frames/uncompress.test.ts deleted file mode 100644 index 889065043b95..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/snappy-frames/uncompress.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {expect} from "chai"; -import {Uint8ArrayList} from "uint8arraylist"; -import snappy from "@chainsafe/snappy-stream"; -import {SnappyFramesUncompress} from "../../../../../../../src/network/reqresp/encodingStrategies/sszSnappy/snappyFrames/uncompress.js"; - -describe("snappy frames uncompress", function () { - it("should work with short input", function (done) { - const compressStream = snappy.createCompressStream(); - - const decompress = new SnappyFramesUncompress(); - - const testData = "Small test data"; - - compressStream.on("data", function (data) { - const result = decompress.uncompress(data); - if (result) { - expect(result.subarray().toString()).to.be.equal(testData); - done(); - } - }); - - compressStream.write(testData); - }); - - it("should work with huge input", function (done) { - const compressStream = snappy.createCompressStream(); - - const decompress = new SnappyFramesUncompress(); - - const testData = Buffer.alloc(100000, 4).toString(); - let result = Buffer.alloc(0); - - compressStream.on("data", function (data) { - // testData will come compressed as two or more chunks - result = Buffer.concat([result, decompress.uncompress(data)?.subarray() ?? Buffer.alloc(0)]); - if (result.length === testData.length) { - expect(result.toString()).to.be.equal(testData); - done(); - } - }); - - compressStream.write(testData); - }); - - it("should detect malformed input", function () { - const decompress = new SnappyFramesUncompress(); - - expect(() => decompress.uncompress(new Uint8ArrayList(Buffer.alloc(32, 5)))).to.throw(); - }); - - it("should return null if not enough data", function () { - const decompress = new SnappyFramesUncompress(); - - expect(decompress.uncompress(new Uint8ArrayList(Buffer.alloc(3, 1)))).to.equal(null); - }); -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/testData.ts b/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/testData.ts deleted file mode 100644 index cb42db4d6166..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/testData.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {Uint8ArrayList} from "uint8arraylist"; -import {fromHexString} from "@chainsafe/ssz"; -import {altair, phase0, ssz} from "@lodestar/types"; -import {RequestOrIncomingResponseBody, RequestOrResponseType} from "../../../../../../src/network/reqresp/types.js"; - -// This test data generated with code from 'master' at Jan 1st 2021 -// commit: ea3ffab1ffb8093b61a8ebfa4b4432c604c10819 - -export interface ISszSnappyTestData { - id: string; - type: RequestOrResponseType; - body: T; - /** chunks expected in an async compress version of snappy stream */ - asyncChunks: Buffer[]; - /** chunks expected in a sync compress version of snappy stream */ - chunks: Uint8ArrayList[]; -} - -export const sszSnappyPing: ISszSnappyTestData = { - id: "Ping type", - type: ssz.phase0.Ping, - body: BigInt(1), - asyncChunks: [ - "0x08", // length prefix - "0xff060000734e61507059", // snappy frames header - "0x010c00000175de410100000000000000", // snappy frames content - ].map(fromHexString) as Buffer[], - chunks: ["0x08", "0xff060000734e61507059010c00000175de410100000000000000"].map( - (s) => new Uint8ArrayList(fromHexString(s)) - ), -}; - -export const sszSnappyStatus: ISszSnappyTestData = { - id: "Status type", - type: ssz.phase0.Status, - body: { - forkDigest: Buffer.alloc(4, 0xda), - finalizedRoot: Buffer.alloc(32, 0xda), - finalizedEpoch: 9, - headRoot: Buffer.alloc(32, 0xda), - headSlot: 9, - }, - asyncChunks: [ - "0x54", // length prefix - "0xff060000734e61507059", // snappy frames header - "0x001b0000097802c15400da8a010004090009017e2b001c0900000000000000", - ].map(fromHexString) as Buffer[], - chunks: ["0x54", "0xff060000734e61507059001b0000097802c15400da8a010004090009017e2b001c0900000000000000"].map( - (s) => new Uint8ArrayList(fromHexString(s)) - ), -}; - -export const sszSnappySignedBeaconBlockPhase0: ISszSnappyTestData = { - id: "SignedBeaconBlock type", - type: ssz.phase0.SignedBeaconBlock, - body: { - message: { - slot: 9, - proposerIndex: 9, - parentRoot: Buffer.alloc(32, 0xda), - stateRoot: Buffer.alloc(32, 0xda), - body: { - randaoReveal: Buffer.alloc(96, 0xda), - eth1Data: { - depositRoot: Buffer.alloc(32, 0xda), - blockHash: Buffer.alloc(32, 0xda), - depositCount: 9, - }, - graffiti: Buffer.alloc(32, 0xda), - proposerSlashings: [], - attesterSlashings: [], - attestations: [], - deposits: [], - voluntaryExits: [], - }, - }, - signature: Buffer.alloc(96, 0xda), - }, - asyncChunks: [ - "0x9403", - "0xff060000734e61507059", - "0x00340000fff3b3f594031064000000dafe01007a010004090009011108fe6f000054feb4008ab4007e0100fecc0011cc0cdc0000003e0400", - ].map(fromHexString) as Buffer[], - chunks: [ - "0x9403", - "0xff060000734e6150705900340000fff3b3f594031064000000dafe01007a010004090009011108fe6f000054feb4008ab4007e0100fecc0011cc0cdc0000003e0400", - ].map((s) => new Uint8ArrayList(fromHexString(s))), -}; - -export const sszSnappySignedBeaconBlockAltair: ISszSnappyTestData = { - id: "SignedBeaconBlock type", - type: ssz.phase0.SignedBeaconBlock, - body: { - ...sszSnappySignedBeaconBlockPhase0.body, - message: { - ...sszSnappySignedBeaconBlockPhase0.body.message, - slot: 90009, - body: { - ...sszSnappySignedBeaconBlockPhase0.body.message.body, - syncAggregate: ssz.altair.SyncAggregate.defaultValue(), - }, - }, - }, - asyncChunks: [ - "0xf803", // length prefix - "0xff060000734e61507059", // snappy frames header - "0x003f0000ee14ab0df8031064000000dafe01007a01000c995f0100010100090105ee70000d700054ee44000d44fe0100fecc0011cc0c400100003e0400fe01008e0100", - ].map(fromHexString) as Buffer[], - chunks: [ - "0xf803", - "0xff060000734e61507059003f0000ee14ab0df8031064000000dafe01007a01000c995f0100010100090105ee70000d700054ee44000d44fe0100fecc0011cc0c400100003e0400fe01008e0100", - ].map((s) => new Uint8ArrayList(fromHexString(s))), -}; diff --git a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/utils.test.ts b/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/utils.test.ts deleted file mode 100644 index 66b92d36602f..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/encodingStrategies/sszSnappy/utils.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {expect} from "chai"; -import {maxEncodedLen} from "../../../../../../src/network/reqresp/encodingStrategies/sszSnappy/utils.js"; - -describe("network / reqresp / sszSnappy / utils", () => { - describe("maxEncodedLen", () => { - it("should calculate correct maxEncodedLen", () => { - expect(maxEncodedLen(6)).to.be.equal(39); - }); - }); -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/rateTracker.test.ts b/packages/beacon-node/test/unit/network/reqresp/rateTracker.test.ts deleted file mode 100644 index ba6f45c3b6f6..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/rateTracker.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {expect} from "chai"; -import sinon from "sinon"; -import {RateTracker} from "../../../../src/network/reqresp/rateTracker.js"; - -describe("RateTracker", () => { - let rateTracker: RateTracker; - const sandbox = sinon.createSandbox(); - - beforeEach(() => { - rateTracker = new RateTracker({limit: 500, timeoutMs: 60 * 1000}); - sandbox.useFakeTimers(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - /** - * A rate tracker with limit 500 and 1 minute track - * - request 300 => ok - * - request 300 => ok - * - request 100 => NOT ok - * - tick 1 minute to reclaim all quota (500) - * - request 100 => ok - * - request 400 => ok - */ - it("should request objects up to limit", () => { - expect(rateTracker.getRequestedObjectsWithinWindow()).to.be.equal(0); - expect(rateTracker.requestObjects(300)).to.be.equal(300); - expect(rateTracker.getRequestedObjectsWithinWindow()).to.be.equal(300); - expect(rateTracker.requestObjects(300)).to.be.equal(300); - expect(rateTracker.getRequestedObjectsWithinWindow()).to.be.equal(600); - expect(rateTracker.requestObjects(100)).to.be.equal(0); - expect(rateTracker.getRequestedObjectsWithinWindow()).to.be.equal(600); - sandbox.clock.tick(60 * 1000); - expect(rateTracker.requestObjects(100)).to.be.equal(100); - expect(rateTracker.getRequestedObjectsWithinWindow()).to.be.equal(100); - expect(rateTracker.requestObjects(400)).to.be.equal(400); - expect(rateTracker.getRequestedObjectsWithinWindow()).to.be.equal(500); - }); - - it.skip("rateTracker memory usage", () => { - const startMem = process.memoryUsage().heapUsed; - // make it full - for (let i = 0; i < 500; i++) { - rateTracker.requestObjects(1); - sandbox.clock.tick(500); - } - // 370k in average - const memUsed = process.memoryUsage().heapUsed - startMem; - expect(memUsed).to.be.lt(400000, "Memory usage per RateTracker should be than 400k"); - }); -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts b/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts deleted file mode 100644 index f2203e58cd2d..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/request/collectResponses.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {expect} from "chai"; -import {collectResponses} from "../../../../../src/network/reqresp/request/collectResponses.js"; -import {ReqRespMethod, IncomingResponseBody} from "../../../../../src/network/reqresp/types.js"; -import {arrToSource} from "../utils.js"; - -describe("network / reqresp / request / collectResponses", () => { - const chunk: IncomingResponseBody = BigInt(1); - - const testCases: { - id: string; - method: ReqRespMethod; - maxResponses?: number; - sourceChunks: IncomingResponseBody[]; - expectedReturn: IncomingResponseBody | IncomingResponseBody[]; - }[] = [ - { - id: "Return first chunk only for a single-chunk method", - method: ReqRespMethod.Ping, - sourceChunks: [chunk, chunk], - expectedReturn: chunk, - }, - { - id: "Return up to maxResponses for a multi-chunk method", - method: ReqRespMethod.BeaconBlocksByRange, - sourceChunks: [chunk, chunk, chunk], - maxResponses: 2, - expectedReturn: [chunk, chunk], - }, - ]; - - for (const {id, method, maxResponses, sourceChunks, expectedReturn} of testCases) { - it(id, async () => { - const responses = await collectResponses(method, maxResponses)(arrToSource(sourceChunks)); - expect(responses).to.deep.equal(expectedReturn); - }); - } -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts b/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts deleted file mode 100644 index a0c4d2740200..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/request/responseTimeoutsHandler.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import {Libp2p} from "libp2p"; -import {Stream} from "@libp2p/interface-connection"; -import {LodestarError, sleep as _sleep} from "@lodestar/utils"; -import {phase0} from "@lodestar/types"; -import {config} from "@lodestar/config/default"; -import {createIBeaconConfig} from "@lodestar/config"; -import {RespStatus, timeoutOptions, ZERO_HASH} from "../../../../../src/constants/index.js"; -import {PeersData} from "../../../../../src/network/peers/peersData.js"; -import { - IRequestErrorMetadata, - RequestError, - RequestErrorCode, -} from "../../../../../src/network/reqresp/request/errors.js"; -import {sendRequest} from "../../../../../src/network/reqresp/request/index.js"; -import {Encoding, ReqRespMethod, Version} from "../../../../../src/network/reqresp/types.js"; -import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; -import {getValidPeerId} from "../../../../utils/peer.js"; -import {testLogger} from "../../../../utils/logger.js"; -import {sszSnappySignedBeaconBlockPhase0} from "../encodingStrategies/sszSnappy/testData.js"; -import {formatProtocolID} from "../../../../../src/network/reqresp/utils/protocolId.js"; - -/* eslint-disable require-yield */ - -describe("network / reqresp / request / responseTimeoutsHandler", () => { - const logger = testLogger(); - - let controller: AbortController; - beforeEach(() => (controller = new AbortController())); - afterEach(() => controller.abort()); - async function sleep(ms: number): Promise { - await _sleep(ms, controller.signal); - } - - // Generic request params not relevant to timeout tests - const method = ReqRespMethod.BeaconBlocksByRange; - const encoding = Encoding.SSZ_SNAPPY; - const version = Version.V1; - const requestBody: phase0.BeaconBlocksByRangeRequest = {startSlot: 0, count: 9, step: 1}; - const maxResponses = requestBody.count; // Random high number - const responseChunk = Buffer.concat([ - Buffer.from([RespStatus.SUCCESS]), - ...sszSnappySignedBeaconBlockPhase0.chunks.map((chunk) => chunk.subarray()), - ]); - const protocol = formatProtocolID(method, version, encoding); - const peerId = getValidPeerId(); - const metadata: IRequestErrorMetadata = {method, encoding, peer: peerId.toString()}; - - /* eslint-disable @typescript-eslint/naming-convention */ - const testCases: { - id: string; - opts?: Partial; - source: () => AsyncGenerator; - error?: LodestarError; - }[] = [ - { - id: "yield values without errors", - source: async function* () { - yield responseChunk.subarray(0, 1); - await sleep(0); - yield responseChunk.subarray(1); - }, - }, - { - id: "trigger a TTFB_TIMEOUT", - opts: {TTFB_TIMEOUT: 0}, - source: async function* () { - await sleep(30); // Pause for too long before first byte - yield responseChunk; - }, - error: new RequestError({code: RequestErrorCode.TTFB_TIMEOUT}, metadata), - }, - { - id: "trigger a RESP_TIMEOUT", - opts: {RESP_TIMEOUT: 0}, - source: async function* () { - yield responseChunk.subarray(0, 1); - await sleep(30); // Pause for too long after first byte - yield responseChunk.subarray(1); - }, - error: new RequestError({code: RequestErrorCode.RESP_TIMEOUT}, metadata), - }, - { - // Upstream "abortable-iterator" never throws with an infinite sleep. - id: "Infinite sleep on first byte", - opts: {TTFB_TIMEOUT: 1, RESP_TIMEOUT: 1}, - source: async function* () { - await sleep(100000); - }, - error: new RequestError({code: RequestErrorCode.TTFB_TIMEOUT}, metadata), - }, - { - id: "Infinite sleep on second chunk", - opts: {TTFB_TIMEOUT: 1, RESP_TIMEOUT: 1}, - source: async function* () { - yield responseChunk; - await sleep(100000); - }, - error: new RequestError({code: RequestErrorCode.RESP_TIMEOUT}, metadata), - }, - ]; - - /* eslint-disable @typescript-eslint/no-empty-function */ - - for (const {id, opts, source, error} of testCases) { - it(id, async () => { - const libp2p = ({ - async dialProtocol() { - return ({ - async sink(): Promise {}, - source: source(), - close() {}, - closeRead() {}, - closeWrite() {}, - abort() {}, - stat: {direction: "outbound", timeline: {open: Date.now()}, protocol}, - } as Partial) as Stream; - }, - } as Partial) as Libp2p; - - const testPromise = sendRequest( - { - logger, - forkDigestContext: createIBeaconConfig(config, ZERO_HASH), - libp2p, - peersData: new PeersData(), - }, - peerId, - method, - encoding, - [version], - requestBody, - maxResponses, - undefined, - opts - ); - - if (error) { - await expectRejectedWithLodestarError(testPromise, error); - } else { - await testPromise; - } - }); - } -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts b/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts deleted file mode 100644 index afea9fdec567..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/response/index.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import {expect} from "chai"; -import {Uint8ArrayList} from "uint8arraylist"; -import {LodestarError, fromHex} from "@lodestar/utils"; -import {RespStatus} from "../../../../../src/constants/index.js"; -import {ReqRespMethod, Encoding, Version} from "../../../../../src/network/reqresp/types.js"; -import {handleRequest, PerformRequestHandler} from "../../../../../src/network/reqresp/response/index.js"; -import {PeersData} from "../../../../../src/network/peers/peersData.js"; -import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; -import {expectEqualByteChunks, MockLibP2pStream} from "../utils.js"; -import {sszSnappyPing} from "../encodingStrategies/sszSnappy/testData.js"; -import {testLogger} from "../../../../utils/logger.js"; -import {getValidPeerId} from "../../../../utils/peer.js"; -import {config} from "../../../../utils/config.js"; - -describe("network / reqresp / response / handleRequest", () => { - const logger = testLogger(); - const peerId = getValidPeerId(); - const peersData = new PeersData(); - - let controller: AbortController; - beforeEach(() => (controller = new AbortController())); - afterEach(() => controller.abort()); - - const testCases: { - id: string; - method: ReqRespMethod; - encoding: Encoding; - requestChunks: Uint8ArrayList[]; - performRequestHandler: PerformRequestHandler; - expectedResponseChunks: Uint8Array[]; - expectedError?: LodestarError; - }[] = [ - { - id: "Yield two chunks, then throw", - method: ReqRespMethod.Ping, - encoding: Encoding.SSZ_SNAPPY, - requestChunks: sszSnappyPing.chunks, // Request Ping: BigInt(1) - performRequestHandler: async function* () { - yield sszSnappyPing.body; - yield sszSnappyPing.body; - throw new LodestarError({code: "TEST_ERROR"}); - }, - expectedError: new LodestarError({code: "TEST_ERROR"}), - expectedResponseChunks: [ - // Chunk 0 - success, Ping, BigInt(1) - Buffer.from([RespStatus.SUCCESS]), - ...sszSnappyPing.chunks.map((c) => c.subarray()), - // Chunk 1 - success, Ping, BigInt(1) - Buffer.from([RespStatus.SUCCESS]), - ...sszSnappyPing.chunks.map((c) => c.subarray()), - // Chunk 2 - error, with errorMessage - Buffer.from([RespStatus.SERVER_ERROR]), - Buffer.from(fromHex("0x0a")), - Buffer.from(fromHex("0xff060000734e61507059010e000049b97aaf544553545f4552524f52")), - ], - }, - ]; - - const version = Version.V1; - - for (const { - id, - method, - encoding, - requestChunks, - performRequestHandler, - expectedResponseChunks, - expectedError, - } of testCases) { - it(id, async () => { - const stream = new MockLibP2pStream(requestChunks); - - const resultPromise = handleRequest( - {config, logger, peersData: peersData}, - performRequestHandler, - stream, - peerId, - {method, version, encoding}, - controller.signal - ); - - // Make sure the test error-ed with expected error, otherwise it's hard to debug with responseChunks - if (expectedError) { - await expectRejectedWithLodestarError(resultPromise, expectedError); - } else { - await expect(resultPromise).to.not.rejectedWith(); - } - - expectEqualByteChunks(stream.resultChunks, expectedResponseChunks, "Wrong response chunks"); - }); - } -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts b/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts deleted file mode 100644 index f05a502fb7c8..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/response/rateLimiter.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import {expect} from "chai"; -import {PeerId} from "@libp2p/interface-peer-id"; -import sinon, {SinonStubbedInstance} from "sinon"; -import {createSecp256k1PeerId} from "@libp2p/peer-id-factory"; -import {IPeerRpcScoreStore, PeerAction, PeerRpcScoreStore} from "../../../../../src/network/index.js"; -import {defaultNetworkOptions} from "../../../../../src/network/options.js"; -import {InboundRateLimiter} from "../../../../../src/network/reqresp/response/rateLimiter.js"; -import {ReqRespMethod, RequestTypedContainer} from "../../../../../src/network/reqresp/types.js"; -import {testLogger} from "../../../../utils/logger.js"; - -describe("ResponseRateLimiter", () => { - const logger = testLogger(); - let inboundRateLimiter: InboundRateLimiter; - const sandbox = sinon.createSandbox(); - let peerRpcScoresStub: IPeerRpcScoreStore & SinonStubbedInstance; - - beforeEach(() => { - peerRpcScoresStub = sandbox.createStubInstance(PeerRpcScoreStore) as IPeerRpcScoreStore & - SinonStubbedInstance; - inboundRateLimiter = new InboundRateLimiter(defaultNetworkOptions, { - logger, - peerRpcScores: peerRpcScoresStub, - metrics: null, - }); - sandbox.useFakeTimers(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - /** - * Steps: - * - Peer1 requests 50 times within 1 minute => ok - * - Peer2 requests 1 time => ok - * - Peer1 requests again => NOT ok, penalty applied - * - Tick 1 minute - * - Peer1 requests again => ok - */ - it("requestCountPeerLimit", async () => { - const peerId = await createSecp256k1PeerId(); - const requestTyped = {method: ReqRespMethod.Ping, body: BigInt(1)} as RequestTypedContainer; - for (let i = 0; i < defaultNetworkOptions.requestCountPeerLimit; i++) { - expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); - } - const peerId2 = await createSecp256k1PeerId(); - // it's ok to request blocks for another peer - expect(inboundRateLimiter.allowRequest(peerId2, requestTyped)).to.equal(true); - expect(peerRpcScoresStub.applyAction).not.to.be.calledOnce; - // not ok for the same peer id as it reached the limit - expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(false); - // this peer id abuses us - expect(peerRpcScoresStub.applyAction, "peer1 is banned due to requestCountPeerLimit").to.be.calledOnceWith( - peerId, - PeerAction.Fatal, - sinon.match.any - ); - - sandbox.clock.tick(60 * 1000); - // try again after timeout - expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); - }); - - /** - * Steps (given block count total limit 2000): - * - Peer1 requests 1000 blocks => ok - * - Peer2 requests 1000 blocks => ok - * - Another peer requests 1 block => NOT ok, no penalty applied - * - Tick 1 minute - * - Another peer requests 1 block => ok - */ - it("blockCountTotalTracker", async () => { - const blockCount = Math.floor(defaultNetworkOptions.blockCountTotalLimit / 2); - const requestTyped = { - method: ReqRespMethod.BeaconBlocksByRange, - body: {count: blockCount}, - } as RequestTypedContainer; - for (let i = 0; i < 2; i++) { - expect(inboundRateLimiter.allowRequest(await createSecp256k1PeerId(), requestTyped)).to.equal(true); - } - - const oneBlockRequestTyped = { - method: ReqRespMethod.BeaconBlocksByRoot, - body: [Buffer.alloc(32)], - } as RequestTypedContainer; - - expect(inboundRateLimiter.allowRequest(await createSecp256k1PeerId(), oneBlockRequestTyped)).to.equal(false); - expect(peerRpcScoresStub.applyAction).not.to.be.calledOnce; - - sandbox.clock.tick(60 * 1000); - // try again after timeout - expect(inboundRateLimiter.allowRequest(await createSecp256k1PeerId(), oneBlockRequestTyped)).to.equal(true); - }); - - /** - * Steps (given default block count peer limit 500): - * - Peer1 requests 250 blocks => ok - * - Peer1 requests 250 blocks => ok - * - Peer2 requests 1 block => ok - * - Peer1 requests 1 block => NOT ok, apply penalty - * - Tick 1 minute - * - Peer1 request 1 block => ok - */ - it("blockCountPeerLimit", async () => { - const blockCount = Math.floor(defaultNetworkOptions.blockCountPeerLimit / 2); - const requestTyped = { - method: ReqRespMethod.BeaconBlocksByRange, - body: {count: blockCount}, - } as RequestTypedContainer; - const peerId = await createSecp256k1PeerId(); - for (let i = 0; i < 2; i++) { - expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); - } - const peerId2 = await createSecp256k1PeerId(); - const oneBlockRequestTyped = { - method: ReqRespMethod.BeaconBlocksByRoot, - body: [Buffer.alloc(32)], - } as RequestTypedContainer; - // it's ok to request blocks for another peer - expect(inboundRateLimiter.allowRequest(peerId2, oneBlockRequestTyped)).to.equal(true); - // not ok for the same peer id as it reached the limit - expect(inboundRateLimiter.allowRequest(peerId, oneBlockRequestTyped)).to.equal(false); - // this peer id abuses us - expect( - peerRpcScoresStub.applyAction.calledOnceWith(peerId, PeerAction.Fatal, sinon.match.any), - "peer1 is banned due to blockCountPeerLimit" - ).to.equal(true); - - sandbox.clock.tick(60 * 1000); - // try again after timeout - expect(inboundRateLimiter.allowRequest(peerId, oneBlockRequestTyped)).to.equal(true); - }); - - it("should remove rate tracker for disconnected peers", async () => { - const peerId = await createSecp256k1PeerId(); - const pruneStub = sandbox.stub(inboundRateLimiter, "pruneByPeerIdStr" as keyof InboundRateLimiter); - inboundRateLimiter.start(); - const requestTyped = {method: ReqRespMethod.Ping, body: BigInt(1)} as RequestTypedContainer; - expect(inboundRateLimiter.allowRequest(peerId, requestTyped)).to.equal(true); - - // no request is made in 5 minutes - sandbox.clock.tick(5 * 60 * 1000); - expect(pruneStub).not.to.be.calledOnce; - // wait for 5 more minutes for the timer to run - sandbox.clock.tick(5 * 60 * 1000); - expect(pruneStub, "prune is not called").to.be.calledOnce; - }); - - it.skip("rateLimiter memory usage", async function () { - this.timeout(5000); - const peerIds: PeerId[] = []; - for (let i = 0; i < 25; i++) { - peerIds.push(await createSecp256k1PeerId()); - } - - const startMem = process.memoryUsage().heapUsed; - - const rateLimiter = new InboundRateLimiter(defaultNetworkOptions, { - logger, - peerRpcScores: peerRpcScoresStub, - metrics: null, - }); - const requestTyped = {method: ReqRespMethod.BeaconBlocksByRoot, body: [Buffer.alloc(32)]} as RequestTypedContainer; - // Make it full: every 1/2s add a new request for all peers - for (let i = 0; i < 1000; i++) { - for (const peerId of peerIds) { - rateLimiter.allowRequest(peerId, requestTyped); - } - sandbox.clock.tick(500); - } - - const memUsage = process.memoryUsage().heapUsed - startMem; - expect(memUsage).to.be.lt(15000000, "memory used for rate limiter should be less than 15MB"); - }); -}); diff --git a/packages/beacon-node/test/unit/network/reqresp/utils.ts b/packages/beacon-node/test/unit/network/reqresp/utils.ts deleted file mode 100644 index c0a2ec67be2b..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/utils.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {expect} from "chai"; -import {Stream, StreamStat} from "@libp2p/interface-connection"; -import {Uint8ArrayList} from "uint8arraylist"; -import {Root, phase0} from "@lodestar/types"; -import {toHexString} from "@chainsafe/ssz"; -import {generateEmptySignedBlock} from "../../../utils/block.js"; - -export function createStatus(): phase0.Status { - return { - finalizedEpoch: 1, - finalizedRoot: Buffer.alloc(32, 0), - forkDigest: Buffer.alloc(4), - headRoot: Buffer.alloc(32, 0), - headSlot: 10, - }; -} - -export function generateRoots(count: number, offset = 0): Root[] { - const roots: Root[] = []; - for (let i = 0; i < count; i++) { - roots.push(Buffer.alloc(32, i + offset)); - } - return roots; -} - -/** - * Helper for it-pipe when first argument is an array. - * it-pipe does not convert the chunks array to a generator and BufferedSource breaks - */ -export async function* arrToSource(arr: T[]): AsyncGenerator { - for (const item of arr) { - yield item; - } -} - -export function generateEmptySignedBlocks(n = 3): phase0.SignedBeaconBlock[] { - return Array.from({length: n}).map(() => generateEmptySignedBlock()); -} - -/** - * Wrapper for type-safety to ensure and array of Buffers is equal with a diff in hex - */ -export function expectEqualByteChunks(chunks: Uint8Array[], expectedChunks: Uint8Array[], message?: string): void { - expect(chunks.map(toHexString)).to.deep.equal(expectedChunks.map(toHexString), message); -} - -/** - * Useful to simulate a LibP2P stream source emitting prepared bytes - * and capture the response with a sink accessible via `this.resultChunks` - */ -export class MockLibP2pStream implements Stream { - id = "mock"; - stat = { - direction: "inbound", - timeline: { - open: Date.now(), - }, - } as StreamStat; - metadata = {}; - source: Stream["source"]; - resultChunks: Uint8Array[] = []; - - constructor(requestChunks: Uint8ArrayList[]) { - this.source = arrToSource(requestChunks); - } - sink: Stream["sink"] = async (source) => { - for await (const chunk of source) { - this.resultChunks.push(chunk.subarray()); - } - }; - // eslint-disable-next-line @typescript-eslint/no-empty-function - close: Stream["close"] = () => {}; - // eslint-disable-next-line @typescript-eslint/no-empty-function - closeRead = (): void => {}; - // eslint-disable-next-line @typescript-eslint/no-empty-function - closeWrite = (): void => {}; - reset: Stream["reset"] = () => this.close(); - abort: Stream["abort"] = () => this.close(); -} diff --git a/packages/beacon-node/test/unit/network/reqresp/utils/assertSequentialBlocksInRange.test.ts b/packages/beacon-node/test/unit/network/reqresp/utils/assertSequentialBlocksInRange.test.ts deleted file mode 100644 index 36b30f678a24..000000000000 --- a/packages/beacon-node/test/unit/network/reqresp/utils/assertSequentialBlocksInRange.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import {phase0} from "@lodestar/types"; -import { - assertSequentialBlocksInRange, - BlocksByRangeError, - BlocksByRangeErrorCode, -} from "../../../../../src/network/reqresp/utils/index.js"; -import {generateEmptySignedBlock} from "../../../../utils/block.js"; -import {expectThrowsLodestarError} from "../../../../utils/errors.js"; - -describe("network / reqResp / utils / assertSequentialBlocksInRange", () => { - const testCases: { - id: string; - slots: number[]; - request: phase0.BeaconBlocksByRangeRequest; - error?: BlocksByRangeError; - }[] = [ - { - id: "Full range", - slots: [1, 2, 3], - request: {startSlot: 1, count: 3, step: 1}, - }, - { - id: "Full range step > 1", - slots: [1, 4, 9], - request: {startSlot: 1, count: 3, step: 3}, - }, - { - id: "Range with skipped slots", - slots: [1, 3], - request: {startSlot: 1, count: 3, step: 1}, - }, - { - id: "Empty range", - slots: [], - request: {startSlot: 1, count: 3, step: 1}, - }, - { - id: "Length too big", - slots: [1, 2, 3, 4], - request: {startSlot: 1, count: 3, step: 1}, - error: new BlocksByRangeError({code: BlocksByRangeErrorCode.BAD_LENGTH, count: 3, length: 4}), - }, - { - id: "Slot under start slot", - slots: [0, 1], - request: {startSlot: 1, count: 3, step: 1}, - error: new BlocksByRangeError({code: BlocksByRangeErrorCode.UNDER_START_SLOT, startSlot: 1, firstSlot: 0}), - }, - { - id: "Slot over max", - slots: [3, 4], - request: {startSlot: 1, count: 3, step: 1}, - error: new BlocksByRangeError({code: BlocksByRangeErrorCode.OVER_MAX_SLOT, maxSlot: 3, lastSlot: 4}), - }, - { - id: "Bad sequence", - slots: [1, 2, 3], - request: {startSlot: 1, count: 3, step: 2}, - error: new BlocksByRangeError({code: BlocksByRangeErrorCode.BAD_SEQUENCE, step: 2, slotL: 1, slotR: 2}), - }, - { - id: "Reverse order", - slots: [3, 2, 1], - request: {startSlot: 1, count: 3, step: 1}, - error: new BlocksByRangeError({code: BlocksByRangeErrorCode.BAD_SEQUENCE, step: 1, slotL: 3, slotR: 2}), - }, - ]; - - for (const {id, slots, request, error} of testCases) { - it(id, () => { - const blocks = slots.map((slot) => { - const block = generateEmptySignedBlock(); - block.message.slot = slot; - return block; - }); - - if (error) { - expectThrowsLodestarError(() => assertSequentialBlocksInRange(blocks, request), error); - } else { - assertSequentialBlocksInRange(blocks, request); - } - }); - } -}); From b279b30f379542a44900512a69eeeb8c32a49d18 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Fri, 18 Nov 2022 23:14:18 +0100 Subject: [PATCH 14/23] Fix some unit tests --- .../test/unit/network/util.test.ts | 45 ------------------ .../test/unit/sync/unknownBlock.test.ts | 6 +-- packages/beacon-node/test/utils/block.ts | 2 +- .../test/utils/mocks/chain/chain.ts | 2 +- packages/reqresp/src/ReqResp.ts | 3 +- packages/reqresp/src/index.ts | 2 +- packages/reqresp/src/types.ts | 13 ++++-- packages/reqresp/src/utils/index.ts | 1 + packages/reqresp/src/utils/protocolId.ts | 43 +++++++++++++++++ .../test/unit/utils/protocolId.test.ts | 46 +++++++++++++++++++ 10 files changed, 107 insertions(+), 56 deletions(-) create mode 100644 packages/reqresp/src/utils/protocolId.ts create mode 100644 packages/reqresp/test/unit/utils/protocolId.test.ts diff --git a/packages/beacon-node/test/unit/network/util.test.ts b/packages/beacon-node/test/unit/network/util.test.ts index 6d1319df6c94..786fdee9926e 100644 --- a/packages/beacon-node/test/unit/network/util.test.ts +++ b/packages/beacon-node/test/unit/network/util.test.ts @@ -4,9 +4,7 @@ import {createSecp256k1PeerId} from "@libp2p/peer-id-factory"; import {config} from "@lodestar/config/default"; import {ForkName} from "@lodestar/params"; import {ENR} from "@chainsafe/discv5"; -import {ReqRespMethod, Version, Encoding, Protocol, protocolPrefix} from "../../../src/network/reqresp/types.js"; import {defaultNetworkOptions} from "../../../src/network/options.js"; -import {formatProtocolID} from "../../../src/network/reqresp/utils/index.js"; import {createNodeJsLibp2p, isLocalMultiAddr} from "../../../src/network/index.js"; import {getCurrentAndNextFork} from "../../../src/network/forks.js"; @@ -22,49 +20,6 @@ describe("Test isLocalMultiAddr", () => { }); }); -describe("ReqResp protocolID parse / render", () => { - const testCases: { - method: ReqRespMethod; - version: Version; - encoding: Encoding; - protocolId: string; - }[] = [ - { - method: ReqRespMethod.Status, - version: Version.V1, - encoding: Encoding.SSZ_SNAPPY, - protocolId: "/eth2/beacon_chain/req/status/1/ssz_snappy", - }, - { - method: ReqRespMethod.BeaconBlocksByRange, - version: Version.V2, - encoding: Encoding.SSZ_SNAPPY, - protocolId: "/eth2/beacon_chain/req/beacon_blocks_by_range/2/ssz_snappy", - }, - ]; - - for (const {method, encoding, version, protocolId} of testCases) { - it(`Should render ${protocolId}`, () => { - expect(formatProtocolID(method, version, encoding)).to.equal(protocolId); - }); - - it(`Should parse ${protocolId}`, () => { - expect(parseProtocolId(protocolId)).to.deep.equal({method, version, encoding}); - }); - } - - function parseProtocolId(protocolId: string): Protocol { - if (!protocolId.startsWith(protocolPrefix)) { - throw Error(`Unknown protocolId prefix: ${protocolId}`); - } - - // +1 for the first "/" - const suffix = protocolId.slice(protocolPrefix.length + 1); - const [method, version, encoding] = suffix.split("/") as [ReqRespMethod, Version, Encoding]; - return {method, version, encoding}; - } -}); - describe("getCurrentAndNextFork", function () { const altairEpoch = config.forks.altair.epoch; afterEach(() => { diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 42355de3efb3..dd7455cc3c6f 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -5,7 +5,7 @@ import {ssz} from "@lodestar/types"; import {notNullish, sleep} from "@lodestar/utils"; import {toHexString} from "@chainsafe/ssz"; import {IBeaconChain} from "../../../src/chain/index.js"; -import {INetwork, IReqResp, NetworkEvent, NetworkEventBus, PeerAction} from "../../../src/network/index.js"; +import {INetwork, IReqRespBeaconNode, NetworkEvent, NetworkEventBus, PeerAction} from "../../../src/network/index.js"; import {UnknownBlockSync} from "../../../src/sync/unknownBlock.js"; import {testLogger} from "../../utils/logger.js"; import {getValidPeerId} from "../../utils/peer.js"; @@ -44,7 +44,7 @@ describe("sync / UnknownBlockSync", () => { [blockRootHexB, blockB], ]); - const reqResp: Partial = { + const reqResp: Partial = { beaconBlocksByRoot: async (_peer, roots) => Array.from(roots) .map((root) => blocksByRoot.get(toHexString(root))) @@ -57,7 +57,7 @@ describe("sync / UnknownBlockSync", () => { const network: Partial = { events: new NetworkEventBus(), getConnectedPeers: () => [peer], - reqResp: reqResp as IReqResp, + reqResp: reqResp as IReqRespBeaconNode, reportPeer: (peerId, action, actionName) => reportPeerResolveFn([peerId, action, actionName]), }; diff --git a/packages/beacon-node/test/utils/block.ts b/packages/beacon-node/test/utils/block.ts index a749db6c3cf4..438fb9908431 100644 --- a/packages/beacon-node/test/utils/block.ts +++ b/packages/beacon-node/test/utils/block.ts @@ -7,7 +7,7 @@ import {ProtoBlock, ExecutionStatus} from "@lodestar/fork-choice"; import {isPlainObject} from "@lodestar/utils"; import {RecursivePartial} from "@lodestar/utils"; import {EMPTY_SIGNATURE, ZERO_HASH} from "../../src/constants/index.js"; -import {ReqRespBlockResponse} from "../../src/network/reqresp/types.js"; +import {ReqRespBlockResponse} from "../../src/network/index.js"; export function generateEmptyBlock(): phase0.BeaconBlock { return { diff --git a/packages/beacon-node/test/utils/mocks/chain/chain.ts b/packages/beacon-node/test/utils/mocks/chain/chain.ts index d0034156f318..de07adb7a567 100644 --- a/packages/beacon-node/test/utils/mocks/chain/chain.ts +++ b/packages/beacon-node/test/utils/mocks/chain/chain.ts @@ -33,7 +33,6 @@ import { import {LightClientServer} from "../../../../src/chain/lightClient/index.js"; import {Eth1ForBlockProductionDisabled} from "../../../../src/eth1/index.js"; import {ExecutionEngineDisabled} from "../../../../src/execution/engine/index.js"; -import {ReqRespBlockResponse} from "../../../../src/network/reqresp/types.js"; import {testLogger} from "../../logger.js"; import {ReprocessController} from "../../../../src/chain/reprocess.js"; import {createCachedBeaconStateTest} from "../../../../../state-transition/test/utils/state.js"; @@ -43,6 +42,7 @@ import {BeaconProposerCache} from "../../../../src/chain/beaconProposerCache.js" import {CheckpointBalancesCache} from "../../../../src/chain/balancesCache.js"; import {IChainOptions} from "../../../../src/chain/options.js"; import {BlockAttributes} from "../../../../src/chain/produceBlock/produceBlockBody.js"; +import {ReqRespBlockResponse} from "../../../../src/network/index.js"; /* eslint-disable @typescript-eslint/no-empty-function */ diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index 8ee3f8be7242..5199dc6092e4 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -7,6 +7,7 @@ import {getMetrics, Metrics, MetricsRegister} from "./metrics.js"; import {RequestError, RequestErrorCode, sendRequest, SendRequestOpts} from "./request/index.js"; import {handleRequest} from "./response/index.js"; import {Encoding, ProtocolDefinition} from "./types.js"; +import {formatProtocolID} from "./utils/protocolId.js"; type ProtocolID = string; @@ -175,6 +176,6 @@ export class ReqResp { * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification */ protected formatProtocolID(method: string, version: number, encoding: Encoding): string { - return `${this.protocolPrefix}/${method}/${version}/${encoding}`; + return formatProtocolID(this.protocolPrefix, method, version, encoding); } } diff --git a/packages/reqresp/src/index.ts b/packages/reqresp/src/index.ts index 6a4dadce7ce8..a7e2583b53c9 100644 --- a/packages/reqresp/src/index.ts +++ b/packages/reqresp/src/index.ts @@ -5,4 +5,4 @@ export * from "./types.js"; export * from "./interface.js"; export {ResponseErrorCode, ResponseError} from "./response/errors.js"; export {RequestErrorCode, RequestError} from "./request/errors.js"; -export {collectExactOne, collectMaxResponse} from "./utils/index.js"; +export {collectExactOne, collectMaxResponse, formatProtocolID, parseProtocolID} from "./utils/index.js"; diff --git a/packages/reqresp/src/types.ts b/packages/reqresp/src/types.ts index 88410ffce644..4efe5b07c740 100644 --- a/packages/reqresp/src/types.ts +++ b/packages/reqresp/src/types.ts @@ -22,12 +22,17 @@ export type EncodedPayload = export type ReqRespHandler = (req: Req, peerId: PeerId) => AsyncIterable>; -export interface ProtocolDefinition { +export interface Protocol { + readonly protocolPrefix: string; /** Protocol name identifier `beacon_blocks_by_range` or `status` */ - method: string; + readonly method: string; /** Version counter: `1`, `2` etc */ - version: number; - encoding: Encoding; + readonly version: number; + readonly encoding: Encoding; +} + +// `protocolPrefix` is added runtime so not part of definition +export interface ProtocolDefinition extends Omit { handler: ReqRespHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any requestType: (fork: ForkName) => TypeSerializer | null; diff --git a/packages/reqresp/src/utils/index.ts b/packages/reqresp/src/utils/index.ts index b454e4695673..d25f3d684c75 100644 --- a/packages/reqresp/src/utils/index.ts +++ b/packages/reqresp/src/utils/index.ts @@ -5,3 +5,4 @@ export * from "./collectMaxResponse.js"; export * from "./errorMessage.js"; export * from "./onChunk.js"; export * from "./peerId.js"; +export * from "./protocolId.js"; diff --git a/packages/reqresp/src/utils/protocolId.ts b/packages/reqresp/src/utils/protocolId.ts new file mode 100644 index 000000000000..6e7b6fb20ec2 --- /dev/null +++ b/packages/reqresp/src/utils/protocolId.ts @@ -0,0 +1,43 @@ +import {Encoding, Protocol} from "../types.js"; + +/** + * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification + */ +export function formatProtocolID(protocolPrefix: string, method: string, version: number, encoding: Encoding): string { + return `${protocolPrefix}/${method}/${version}/${encoding}`; +} + +/** + * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification + */ +export function parseProtocolID(protocolId: string): Protocol { + const result = protocolId.split("/"); + if (result.length < 4) { + throw new Error(`Invalid protocol id: ${protocolId}`); + } + + const encoding = result[result.length - 1] as Encoding; + if (!Object.values(Encoding).includes(encoding)) { + throw new Error(`Invalid protocol encoding: ${result[result.length - 1]}`); + } + + if (!/^-?[0-9]+$/.test(result[result.length - 2])) { + throw new Error(`Invalid protocol version: ${result[result.length - 2]}`); + } + + // an ordinal version number (e.g. 1, 2, 3…). + const version = parseInt(result[result.length - 2]); + + // each request is identified by a name consisting of English alphabet, digits and underscores (_). + const method = result[result.length - 3]; + + // messages are grouped into families identified by a shared libp2p protocol name prefix + const protocolPrefix = result.slice(0, result.length - 3).join("/"); + + return { + protocolPrefix, + method, + version, + encoding, + }; +} diff --git a/packages/reqresp/test/unit/utils/protocolId.test.ts b/packages/reqresp/test/unit/utils/protocolId.test.ts new file mode 100644 index 000000000000..b418c42020d4 --- /dev/null +++ b/packages/reqresp/test/unit/utils/protocolId.test.ts @@ -0,0 +1,46 @@ +import {expect} from "chai"; +import {Encoding, Protocol} from "../../../src/index.js"; +import {formatProtocolID, parseProtocolID as reqrespParseProtocolID} from "../../../src/utils/index.js"; + +const protocolPrefix = "/eth2/beacon_chain/req"; + +function parseProtocolId(protocolId: string): Protocol { + const result = reqrespParseProtocolID(protocolId); + if (result.protocolPrefix !== protocolPrefix) { + throw Error(`Unknown protocolId prefix: ${result.protocolPrefix}`); + } + + return result; +} + +describe("ReqResp protocolID parse / render", () => { + const testCases: { + method: string; + version: number; + encoding: Encoding; + protocolId: string; + }[] = [ + { + method: "status", + version: 1, + encoding: Encoding.SSZ_SNAPPY, + protocolId: "/eth2/beacon_chain/req/status/1/ssz_snappy", + }, + { + method: "beacon_blocks_by_range", + version: 2, + encoding: Encoding.SSZ_SNAPPY, + protocolId: "/eth2/beacon_chain/req/beacon_blocks_by_range/2/ssz_snappy", + }, + ]; + + for (const {method, encoding, version, protocolId} of testCases) { + it(`Should render ${protocolId}`, () => { + expect(formatProtocolID(protocolPrefix, method, version, encoding)).to.equal(protocolId); + }); + + it(`Should parse ${protocolId}`, () => { + expect(parseProtocolId(protocolId)).to.deep.equal({protocolPrefix, method, version, encoding}); + }); + } +}); From b38f32fcf6c75c2560ddefe6c4873574cadd9f51 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Fri, 18 Nov 2022 23:30:51 +0100 Subject: [PATCH 15/23] Fix few performance tests --- packages/beacon-node/package.json | 1 - .../beacon-node/test/perf/network/reqresp/rateTracker.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index fb3eca510f08..27ead3e39eb4 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -86,7 +86,6 @@ "test:unit": "yarn test:unit:minimal && yarn test:unit:mainnet", "test:e2e": "mocha 'test/e2e/**/*.test.ts'", "test:sim": "mocha 'test/sim/**/*.test.ts'", - "test:sim:multiThread": "mocha 'test/sim/multiNodeMultiThread.test.ts'", "test:sim:merge-interop": "mocha 'test/sim/merge-interop.test.ts'", "test:sim:mergemock": "mocha 'test/sim/mergemock.test.ts'", "download-spec-tests": "node --loader=ts-node/esm test/spec/downloadTests.ts", diff --git a/packages/beacon-node/test/perf/network/reqresp/rateTracker.test.ts b/packages/beacon-node/test/perf/network/reqresp/rateTracker.test.ts index 6c3fffa02fe4..2ff199f0012f 100644 --- a/packages/beacon-node/test/perf/network/reqresp/rateTracker.test.ts +++ b/packages/beacon-node/test/perf/network/reqresp/rateTracker.test.ts @@ -1,6 +1,6 @@ import {itBench} from "@dapplion/benchmark"; import {MapDef} from "@lodestar/utils"; -import {defaultNetworkOptions} from "../../../../src/network/options.js"; +import {defaultRateLimiterOpts} from "../../../../lib/network/reqresp/inboundRateLimiter.js"; import {RateTracker} from "../../../../src/network/reqresp/rateTracker.js"; /** @@ -8,7 +8,7 @@ import {RateTracker} from "../../../../src/network/reqresp/rateTracker.js"; * But adding sinon mock timer here make it impossible to benchmark. */ describe("RateTracker", () => { - const {rateTrackerTimeoutMs} = defaultNetworkOptions; + const {rateTrackerTimeoutMs} = defaultRateLimiterOpts; const iteration = 1_000_000; // from object count per request make it fit the quota From 33db23578f208e4b5dc23bf059af2c1e28a9077d Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Fri, 18 Nov 2022 23:32:49 +0100 Subject: [PATCH 16/23] Fix tsonfig for reqresp package --- packages/reqresp/tsconfig.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/reqresp/tsconfig.json b/packages/reqresp/tsconfig.json index ed997ec031fa..4feb265109be 100644 --- a/packages/reqresp/tsconfig.json +++ b/packages/reqresp/tsconfig.json @@ -1,5 +1,12 @@ { "extends": "../../tsconfig.json", - "exclude": ["../../node_modules/it-pipe"], - "typeRoots": ["../../node_modules/@types", "../../types"] -} + "exclude": [ + "../../node_modules/it-pipe" + ], + "compilerOptions": { + "typeRoots": [ + "../../node_modules/@types", + "../../types" + ] + }, +} \ No newline at end of file From 79bb6b5be639c2196507bf00a65bf457914a09b3 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sat, 19 Nov 2022 00:20:15 +0100 Subject: [PATCH 17/23] Fix e2e tests --- .../e2e/network/peers/peerManager.test.ts | 5 +- .../test/e2e/network/reqresp.test.ts | 75 ++++++++++-------- .../test/unit/network/reqresp/utils.ts | 79 +++++++++++++++++++ packages/reqresp/src/index.ts | 2 +- 4 files changed, 123 insertions(+), 38 deletions(-) create mode 100644 packages/beacon-node/test/unit/network/reqresp/utils.ts diff --git a/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts b/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts index b634aee2102e..d5dba11f5018 100644 --- a/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts +++ b/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts @@ -8,7 +8,7 @@ import {BitArray} from "@chainsafe/ssz"; import {altair, phase0, ssz} from "@lodestar/types"; import {sleep} from "@lodestar/utils"; import {createIBeaconConfig} from "@lodestar/config"; -import {IReqResp, ReqRespMethod} from "../../../../src/network/reqresp/index.js"; +import {IReqRespBeaconNode, ReqRespMethod} from "../../../../src/network/reqresp/index.js"; import {PeerRpcScoreStore, PeerManager} from "../../../../src/network/peers/index.js"; import {Eth2Gossipsub, getConnectionsMap, NetworkEvent, NetworkEventBus} from "../../../../src/network/index.js"; import {PeersData} from "../../../../src/network/peers/peersData.js"; @@ -101,7 +101,7 @@ describe("network / peers / PeerManager", function () { } // Create a real event emitter with stubbed methods - class ReqRespFake implements IReqResp { + class ReqRespFake implements IReqRespBeaconNode { start = sinon.stub(); stop = sinon.stub(); status = sinon.stub(); @@ -115,6 +115,7 @@ describe("network / peers / PeerManager", function () { lightClientOptimisticUpdate = sinon.stub(); lightClientFinalityUpdate = sinon.stub(); lightClientUpdate = sinon.stub(); + lightClientUpdatesByRange = sinon.stub(); } it("Should request metadata on receivedPing of unknown peer", async () => { diff --git a/packages/beacon-node/test/e2e/network/reqresp.test.ts b/packages/beacon-node/test/e2e/network/reqresp.test.ts index 2151184aa668..6bdef44828fa 100644 --- a/packages/beacon-node/test/e2e/network/reqresp.test.ts +++ b/packages/beacon-node/test/e2e/network/reqresp.test.ts @@ -1,32 +1,37 @@ -import {expect} from "chai"; import {PeerId} from "@libp2p/interface-peer-id"; import {createSecp256k1PeerId} from "@libp2p/peer-id-factory"; +import {expect} from "chai"; +import {BitArray} from "@chainsafe/ssz"; import {createIBeaconConfig} from "@lodestar/config"; import {config} from "@lodestar/config/default"; -import {sleep as _sleep} from "@lodestar/utils"; -import {altair, phase0, Root, ssz} from "@lodestar/types"; import {ForkName} from "@lodestar/params"; -import {BitArray} from "@chainsafe/ssz"; -import {IReqRespOptions, Network} from "../../../src/network/index.js"; +import { + Encoding, + RequestError, + RequestErrorCode, + IRequestErrorMetadata, + HandlerTypeFromMessage, +} from "@lodestar/reqresp"; +import * as messages from "@lodestar/reqresp/messages"; +import {altair, phase0, Root, ssz} from "@lodestar/types"; +import {sleep as _sleep} from "@lodestar/utils"; +import {GossipHandlers} from "../../../src/network/gossip/index.js"; +import {Network, ReqRespBeaconNodeOpts} from "../../../src/network/index.js"; import {defaultNetworkOptions, INetworkOptions} from "../../../src/network/options.js"; -import {ReqRespMethod, Encoding} from "../../../src/network/reqresp/types.js"; import {ReqRespHandlers} from "../../../src/network/reqresp/handlers/index.js"; -import {RequestError, RequestErrorCode} from "../../../src/network/reqresp/request/index.js"; -import {IRequestErrorMetadata} from "../../../src/network/reqresp/request/errors.js"; -import {testLogger} from "../../utils/logger.js"; -import {MockBeaconChain} from "../../utils/mocks/chain/chain.js"; -import {createNode} from "../../utils/network.js"; -import {generateState} from "../../utils/state.js"; -import {arrToSource, generateEmptySignedBlocks} from "../../unit/network/reqresp/utils.js"; +import {ReqRespMethod} from "../../../src/network/reqresp/types.js"; import { blocksToReqRespBlockResponses, generateEmptyReqRespBlockResponse, generateEmptySignedBlock, } from "../../utils/block.js"; import {expectRejectedWithLodestarError} from "../../utils/errors.js"; -import {connect, onPeerConnect} from "../../utils/network.js"; +import {testLogger} from "../../utils/logger.js"; +import {MockBeaconChain} from "../../utils/mocks/chain/chain.js"; +import {connect, createNode, onPeerConnect} from "../../utils/network.js"; +import {generateState} from "../../utils/state.js"; import {StubbedBeaconDb} from "../../utils/stub/index.js"; -import {GossipHandlers} from "../../../src/network/gossip/index.js"; +import {arrToSource, generateEmptySignedBlocks} from "../../unit/network/reqresp/utils.js"; /* eslint-disable require-yield, @typescript-eslint/naming-convention */ @@ -66,7 +71,7 @@ describe("network / ReqResp", function () { async function createAndConnectPeers( reqRespHandlersPartial?: Partial, - reqRespOpts?: IReqRespOptions + reqRespOpts?: ReqRespBeaconNodeOpts ): Promise<[Network, Network]> { const controller = new AbortController(); const peerIdB = await createSecp256k1PeerId(); @@ -168,7 +173,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onStatus: async function* onRequest() { yield statusNetB; - }, + } as HandlerTypeFromMessage, }); const receivedStatus = await netA.reqResp.status(netB.peerId, statusNetA); @@ -187,7 +192,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onBeaconBlocksByRange: async function* () { yield* arrToSource(blocksToReqRespBlockResponses(blocks)); - }, + } as HandlerTypeFromMessage, }); const returnedBlocks = await netA.reqResp.beaconBlocksByRange(netB.peerId, req); @@ -207,7 +212,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientBootstrap: async function* onRequest() { yield expectedValue; - }, + } as HandlerTypeFromMessage, }); const returnedValue = await netA.reqResp.lightClientBootstrap(netB.peerId, root); @@ -220,7 +225,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientOptimisticUpdate: async function* onRequest() { yield expectedValue; - }, + } as HandlerTypeFromMessage, }); const returnedValue = await netA.reqResp.lightClientOptimisticUpdate(netB.peerId); @@ -233,7 +238,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientFinalityUpdate: async function* onRequest() { yield expectedValue; - }, + } as HandlerTypeFromMessage, }); const returnedValue = await netA.reqResp.lightClientFinalityUpdate(netB.peerId); @@ -252,10 +257,10 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientUpdatesByRange: async function* () { yield* arrToSource(lightClientUpdates); - }, + } as HandlerTypeFromMessage, }); - const returnedUpdates = await netA.reqResp.lightClientUpdate(netB.peerId, req); + const returnedUpdates = await netA.reqResp.lightClientUpdatesByRange(netB.peerId, req); if (returnedUpdates === null) throw Error("Returned null"); expect(returnedUpdates).to.have.length(2, "Wrong returnedUpdates length"); @@ -292,7 +297,7 @@ describe("network / ReqResp", function () { onBeaconBlocksByRange: async function* onRequest() { yield* arrToSource(blocksToReqRespBlockResponses(generateEmptySignedBlocks(2))); throw Error(testErrorMessage); - }, + } as HandlerTypeFromMessage, }); await expectRejectedWithLodestarError( @@ -305,17 +310,17 @@ describe("network / ReqResp", function () { }); it("trigger a TTFB_TIMEOUT error", async function () { - const TTFB_TIMEOUT = 250; + const ttfbTimeoutMs = 250; const [netA, netB] = await createAndConnectPeers( { onBeaconBlocksByRange: async function* onRequest() { // Wait for too long before sending first response chunk - await sleep(TTFB_TIMEOUT * 10); + await sleep(ttfbTimeoutMs * 10); yield generateEmptyReqRespBlockResponse(); - }, + } as HandlerTypeFromMessage, }, - {TTFB_TIMEOUT} + {ttfbTimeoutMs: ttfbTimeoutMs} ); await expectRejectedWithLodestarError( @@ -328,18 +333,18 @@ describe("network / ReqResp", function () { }); it("trigger a RESP_TIMEOUT error", async function () { - const RESP_TIMEOUT = 250; + const respTimeoutMs = 250; const [netA, netB] = await createAndConnectPeers( { onBeaconBlocksByRange: async function* onRequest() { yield generateEmptyReqRespBlockResponse(); // Wait for too long before sending second response chunk - await sleep(RESP_TIMEOUT * 5); + await sleep(respTimeoutMs * 5); yield generateEmptyReqRespBlockResponse(); - }, + } as HandlerTypeFromMessage, }, - {RESP_TIMEOUT} + {respTimeoutMs} ); await expectRejectedWithLodestarError( @@ -358,7 +363,7 @@ describe("network / ReqResp", function () { await sleep(100000000); }, }, - {RESP_TIMEOUT: 250, TTFB_TIMEOUT: 250} + {respTimeoutMs: 250, ttfbTimeoutMs: 250} ); await expectRejectedWithLodestarError( @@ -376,9 +381,9 @@ describe("network / ReqResp", function () { onBeaconBlocksByRange: async function* onRequest() { yield generateEmptyReqRespBlockResponse(); await sleep(100000000); - }, + } as HandlerTypeFromMessage, }, - {RESP_TIMEOUT: 250, TTFB_TIMEOUT: 250} + {respTimeoutMs: 250, ttfbTimeoutMs: 250} ); await expectRejectedWithLodestarError( diff --git a/packages/beacon-node/test/unit/network/reqresp/utils.ts b/packages/beacon-node/test/unit/network/reqresp/utils.ts new file mode 100644 index 000000000000..e6c88c1ef29f --- /dev/null +++ b/packages/beacon-node/test/unit/network/reqresp/utils.ts @@ -0,0 +1,79 @@ +import {expect} from "chai"; +import {Stream, StreamStat} from "@libp2p/interface-connection"; +import {Uint8ArrayList} from "uint8arraylist"; +import {Root, phase0} from "@lodestar/types"; +import {toHexString} from "@chainsafe/ssz"; +import {generateEmptySignedBlock} from "../../../utils/block.js"; + +export function createStatus(): phase0.Status { + return { + finalizedEpoch: 1, + finalizedRoot: Buffer.alloc(32, 0), + forkDigest: Buffer.alloc(4), + headRoot: Buffer.alloc(32, 0), + headSlot: 10, + }; +} + +export function generateRoots(count: number, offset = 0): Root[] { + const roots: Root[] = []; + for (let i = 0; i < count; i++) { + roots.push(Buffer.alloc(32, i + offset)); + } + return roots; +} + +/** + * Helper for it-pipe when first argument is an array. + * it-pipe does not convert the chunks array to a generator and BufferedSource breaks + */ +export async function* arrToSource(arr: T[]): AsyncGenerator { + for (const item of arr) { + yield item; + } +} + +export function generateEmptySignedBlocks(n = 3): phase0.SignedBeaconBlock[] { + return Array.from({length: n}).map(() => generateEmptySignedBlock()); +} + +/** + * Wrapper for type-safety to ensure and array of Buffers is equal with a diff in hex + */ +export function expectEqualByteChunks(chunks: Uint8Array[], expectedChunks: Uint8Array[], message?: string): void { + expect(chunks.map(toHexString)).to.deep.equal(expectedChunks.map(toHexString), message); +} + +/** + * Useful to simulate a LibP2P stream source emitting prepared bytes + * and capture the response with a sink accessible via `this.resultChunks` + */ +export class MockLibP2pStream implements Stream { + id = "mock"; + stat = { + direction: "inbound", + timeline: { + open: Date.now(), + }, + } as StreamStat; + metadata = {}; + source: Stream["source"]; + resultChunks: Uint8Array[] = []; + + constructor(requestChunks: Uint8ArrayList[]) { + this.source = arrToSource(requestChunks); + } + sink: Stream["sink"] = async (source) => { + for await (const chunk of source) { + this.resultChunks.push(chunk.subarray()); + } + }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + close: Stream["close"] = () => {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + closeRead = (): void => {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + closeWrite = (): void => {}; + reset: Stream["reset"] = () => this.close(); + abort: Stream["abort"] = () => this.close(); +} \ No newline at end of file diff --git a/packages/reqresp/src/index.ts b/packages/reqresp/src/index.ts index a7e2583b53c9..bcdb5e7c9513 100644 --- a/packages/reqresp/src/index.ts +++ b/packages/reqresp/src/index.ts @@ -4,5 +4,5 @@ export {Encoding as ReqRespEncoding} from "./types.js"; // Expose enums renamed export * from "./types.js"; export * from "./interface.js"; export {ResponseErrorCode, ResponseError} from "./response/errors.js"; -export {RequestErrorCode, RequestError} from "./request/errors.js"; +export {RequestErrorCode, RequestError, IRequestErrorMetadata} from "./request/errors.js"; export {collectExactOne, collectMaxResponse, formatProtocolID, parseProtocolID} from "./utils/index.js"; From a96d517f48068ac36f270a1198f4ad430d078ff9 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sat, 19 Nov 2022 00:37:05 +0100 Subject: [PATCH 18/23] Fix a type error --- packages/validator/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validator/tsconfig.json b/packages/validator/tsconfig.json index c5f850d41f4f..f81823701532 100644 --- a/packages/validator/tsconfig.json +++ b/packages/validator/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "typeRoots": ["../../node_modules/@types", "./node_modules/@types"] + "typeRoots": ["../../node_modules/@types", "./node_modules/@types", "../../types"] } } From ae4ac188ae697a13caa1dd59c0a571036860e198 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sun, 20 Nov 2022 01:48:59 +0100 Subject: [PATCH 19/23] Fix few e2e tests --- .../beacon-node/src/network/reqresp/index.ts | 7 +-- .../test/e2e/network/reqresp.test.ts | 50 ++++++++++++++----- .../test/unit/network/reqresp/utils.ts | 4 +- packages/beacon-node/test/utils/block.ts | 25 +++++++--- packages/beacon-node/test/utils/errors.ts | 6 ++- .../test/utils/mocks/chain/chain.ts | 11 ++-- .../messages/LightClientOptimisticUpdate.ts | 2 +- 7 files changed, 72 insertions(+), 33 deletions(-) diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index e8b6111b8a30..f522709e2f10 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -35,11 +35,7 @@ export {ReqRespMethod, RequestTypedContainer} from "./types.js"; export {getReqRespHandlers, ReqRespHandlers} from "./handlers/index.js"; /** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ -export type ReqRespBlockResponse = { - /** Deserialized data of allForks.SignedBeaconBlock */ - bytes: Uint8Array; - slot: Slot; -}; +export type ReqRespBlockResponse = EncodedPayload; export interface ReqRespBeaconNodeModules { libp2p: Libp2p; @@ -272,6 +268,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body: req}, peerId); + yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; } diff --git a/packages/beacon-node/test/e2e/network/reqresp.test.ts b/packages/beacon-node/test/e2e/network/reqresp.test.ts index 6bdef44828fa..8a8465d16cfb 100644 --- a/packages/beacon-node/test/e2e/network/reqresp.test.ts +++ b/packages/beacon-node/test/e2e/network/reqresp.test.ts @@ -11,6 +11,8 @@ import { RequestErrorCode, IRequestErrorMetadata, HandlerTypeFromMessage, + EncodedPayloadType, + EncodedPayload, } from "@lodestar/reqresp"; import * as messages from "@lodestar/reqresp/messages"; import {altair, phase0, Root, ssz} from "@lodestar/types"; @@ -32,6 +34,7 @@ import {connect, createNode, onPeerConnect} from "../../utils/network.js"; import {generateState} from "../../utils/state.js"; import {StubbedBeaconDb} from "../../utils/stub/index.js"; import {arrToSource, generateEmptySignedBlocks} from "../../unit/network/reqresp/utils.js"; +import {defaultRateLimiterOpts} from "../../../src/network/reqresp/inboundRateLimiter.js"; /* eslint-disable require-yield, @typescript-eslint/naming-convention */ @@ -42,6 +45,7 @@ describe("network / ReqResp", function () { const multiaddr = "/ip4/127.0.0.1/tcp/0"; const networkOptsDefault: INetworkOptions = { ...defaultNetworkOptions, + ...defaultRateLimiterOpts, maxPeers: 1, targetPeers: 1, bootMultiaddrs: [], @@ -83,7 +87,12 @@ describe("network / ReqResp", function () { }; const reqRespHandlers: ReqRespHandlers = { - onStatus: notImplemented, + onStatus: async function* onRequest() { + yield { + type: EncodedPayloadType.ssz, + data: chain.getStatus(), + }; + } as HandlerTypeFromMessage, onBeaconBlocksByRange: notImplemented, onBeaconBlocksByRoot: notImplemented, onLightClientBootstrap: notImplemented, @@ -172,7 +181,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onStatus: async function* onRequest() { - yield statusNetB; + yield {type: EncodedPayloadType.ssz, data: statusNetB}; } as HandlerTypeFromMessage, }); @@ -211,7 +220,10 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientBootstrap: async function* onRequest() { - yield expectedValue; + yield { + type: EncodedPayloadType.ssz, + data: expectedValue, + }; } as HandlerTypeFromMessage, }); @@ -224,7 +236,10 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientOptimisticUpdate: async function* onRequest() { - yield expectedValue; + yield { + type: EncodedPayloadType.ssz, + data: expectedValue, + }; } as HandlerTypeFromMessage, }); @@ -237,7 +252,10 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onLightClientFinalityUpdate: async function* onRequest() { - yield expectedValue; + yield { + type: EncodedPayloadType.ssz, + data: expectedValue, + }; } as HandlerTypeFromMessage, }); @@ -247,11 +265,14 @@ describe("network / ReqResp", function () { it("should send/receive a light client update message", async function () { const req: altair.LightClientUpdatesByRange = {startPeriod: 0, count: 2}; - const lightClientUpdates: altair.LightClientUpdate[] = []; + const lightClientUpdates: EncodedPayload[] = []; for (let slot = req.startPeriod; slot < req.count; slot++) { const update = ssz.altair.LightClientUpdate.defaultValue(); update.signatureSlot = slot; - lightClientUpdates.push(update); + lightClientUpdates.push({ + type: EncodedPayloadType.ssz, + data: update, + }); } const [netA, netB] = await createAndConnectPeers({ @@ -266,10 +287,15 @@ describe("network / ReqResp", function () { expect(returnedUpdates).to.have.length(2, "Wrong returnedUpdates length"); for (const [i, returnedUpdate] of returnedUpdates.entries()) { - expect(ssz.altair.LightClientUpdate.equals(returnedUpdate, lightClientUpdates[i])).to.equal( - true, - `Wrong returnedUpdate[${i}]` - ); + expect( + ssz.altair.LightClientUpdate.equals( + returnedUpdate, + (lightClientUpdates[i] as { + type: EncodedPayloadType.ssz; + data: altair.LightClientUpdate; + }).data + ) + ).to.equal(true, `Wrong returnedUpdate[${i}]`); } }); @@ -320,7 +346,7 @@ describe("network / ReqResp", function () { yield generateEmptyReqRespBlockResponse(); } as HandlerTypeFromMessage, }, - {ttfbTimeoutMs: ttfbTimeoutMs} + {ttfbTimeoutMs} ); await expectRejectedWithLodestarError( diff --git a/packages/beacon-node/test/unit/network/reqresp/utils.ts b/packages/beacon-node/test/unit/network/reqresp/utils.ts index e6c88c1ef29f..ef1df9b39f62 100644 --- a/packages/beacon-node/test/unit/network/reqresp/utils.ts +++ b/packages/beacon-node/test/unit/network/reqresp/utils.ts @@ -34,7 +34,7 @@ export async function* arrToSource(arr: T[]): AsyncGenerator { } export function generateEmptySignedBlocks(n = 3): phase0.SignedBeaconBlock[] { - return Array.from({length: n}).map(() => generateEmptySignedBlock()); + return Array.from({length: n}).map((_, i) => generateEmptySignedBlock(i)); } /** @@ -76,4 +76,4 @@ export class MockLibP2pStream implements Stream { closeWrite = (): void => {}; reset: Stream["reset"] = () => this.close(); abort: Stream["abort"] = () => this.close(); -} \ No newline at end of file +} diff --git a/packages/beacon-node/test/utils/block.ts b/packages/beacon-node/test/utils/block.ts index 438fb9908431..50c0fc024a21 100644 --- a/packages/beacon-node/test/utils/block.ts +++ b/packages/beacon-node/test/utils/block.ts @@ -1,17 +1,18 @@ import deepmerge from "deepmerge"; -import {ssz} from "@lodestar/types"; +import {Slot, ssz} from "@lodestar/types"; import {config as defaultConfig} from "@lodestar/config/default"; import {IChainForkConfig} from "@lodestar/config"; import {allForks, phase0} from "@lodestar/types"; import {ProtoBlock, ExecutionStatus} from "@lodestar/fork-choice"; import {isPlainObject} from "@lodestar/utils"; import {RecursivePartial} from "@lodestar/utils"; +import {ContextBytesType, EncodedPayloadType} from "@lodestar/reqresp"; import {EMPTY_SIGNATURE, ZERO_HASH} from "../../src/constants/index.js"; import {ReqRespBlockResponse} from "../../src/network/index.js"; -export function generateEmptyBlock(): phase0.BeaconBlock { +export function generateEmptyBlock(slot: Slot = 0): phase0.BeaconBlock { return { - slot: 0, + slot, proposerIndex: 0, parentRoot: Buffer.alloc(32), stateRoot: ZERO_HASH, @@ -32,17 +33,23 @@ export function generateEmptyBlock(): phase0.BeaconBlock { }; } -export function generateEmptySignedBlock(): phase0.SignedBeaconBlock { +export function generateEmptySignedBlock(slot: Slot = 0): phase0.SignedBeaconBlock { return { - message: generateEmptyBlock(), + message: generateEmptyBlock(slot), signature: EMPTY_SIGNATURE, }; } export function generateEmptyReqRespBlockResponse(): ReqRespBlockResponse { + const block = generateEmptySignedBlock(); + return { - slot: 0, + type: EncodedPayloadType.bytes, bytes: Buffer.from(ssz.phase0.SignedBeaconBlock.serialize(generateEmptySignedBlock())), + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.message.slot, + }, }; } @@ -56,8 +63,12 @@ export function blocksToReqRespBlockResponses( ? config.getForkTypes(slot).SignedBeaconBlock : defaultConfig.getForkTypes(slot).SignedBeaconBlock; return { - slot, + type: EncodedPayloadType.bytes, bytes: Buffer.from(sszType.serialize(block)), + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.message.slot, + }, }; }); } diff --git a/packages/beacon-node/test/utils/errors.ts b/packages/beacon-node/test/utils/errors.ts index b33978058fa8..c6cec8bc1392 100644 --- a/packages/beacon-node/test/utils/errors.ts +++ b/packages/beacon-node/test/utils/errors.ts @@ -40,8 +40,10 @@ export function expectLodestarErrorCode(err: LodestarE } export function expectLodestarError(err1: LodestarError, err2: LodestarError): void { - if (!(err1 instanceof LodestarError)) throw Error(`err1 not instanceof LodestarError: ${(err1 as Error).stack}`); - if (!(err2 instanceof LodestarError)) throw Error(`err2 not instanceof LodestarError: ${(err2 as Error).stack}`); + if (!(err1 instanceof LodestarError)) + throw Error(`err1(${err1}) not instanceof LodestarError: ${(err1 as Error).stack}`); + if (!(err2 instanceof LodestarError)) + throw Error(`err2(${err2}) not instanceof LodestarError: ${(err2 as Error).stack}`); const errMeta1 = getErrorMetadata(err1); const errMeta2 = getErrorMetadata(err2); diff --git a/packages/beacon-node/test/utils/mocks/chain/chain.ts b/packages/beacon-node/test/utils/mocks/chain/chain.ts index de07adb7a567..ef4e32bed76a 100644 --- a/packages/beacon-node/test/utils/mocks/chain/chain.ts +++ b/packages/beacon-node/test/utils/mocks/chain/chain.ts @@ -8,6 +8,7 @@ import {CheckpointWithHex, IForkChoice, ProtoBlock, ExecutionStatus} from "@lode import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; import {ILogger} from "@lodestar/utils"; +import {ContextBytesType, EncodedPayloadType} from "@lodestar/reqresp"; import {ChainEventEmitter, IBeaconChain} from "../../../../src/chain/index.js"; import {IBeaconClock} from "../../../../src/chain/clock/interface.js"; import {generateEmptySignedBlock} from "../../block.js"; @@ -164,16 +165,18 @@ export class MockBeaconChain implements IBeaconChain { } async getCanonicalBlockAtSlot(slot: Slot): Promise { - const block = generateEmptySignedBlock(); - block.message.slot = slot; - return block; + return generateEmptySignedBlock(slot); } async getUnfinalizedBlocksAtSlots(slots: Slot[] = []): Promise { const blocks = await Promise.all(slots.map(this.getCanonicalBlockAtSlot)); return blocks.map((block, i) => ({ - slot: slots[i], + type: EncodedPayloadType.bytes, bytes: Buffer.from(ssz.phase0.SignedBeaconBlock.serialize(block)), + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: slots[i], + }, })); } diff --git a/packages/reqresp/src/messages/LightClientOptimisticUpdate.ts b/packages/reqresp/src/messages/LightClientOptimisticUpdate.ts index d2f6d0849e20..0f0eca8eb4b3 100644 --- a/packages/reqresp/src/messages/LightClientOptimisticUpdate.ts +++ b/packages/reqresp/src/messages/LightClientOptimisticUpdate.ts @@ -8,7 +8,7 @@ export const LightClientOptimisticUpdate: ProtocolDefinitionGenerator { return { - method: "light_client_finality_update", + method: "light_client_optimistic_update", version: 1, encoding: Encoding.SSZ_SNAPPY, handler, From 848b5438a52e279cf2b77640dd918f7ad49d8f99 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sun, 20 Nov 2022 02:14:03 +0100 Subject: [PATCH 20/23] Fix e2e timeout tests --- packages/beacon-node/src/network/reqresp/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index f522709e2f10..28ec907185fb 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -273,7 +273,7 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { } private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body: req}, peerId); + this.onIncomingRequestBody({method: ReqRespMethod.Ping, body: req}, peerId); yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; } From 02a04a4bbfe930d4836810900d3c0e6a2216e7e9 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sun, 20 Nov 2022 02:38:37 +0100 Subject: [PATCH 21/23] Fix readme --- packages/reqresp/README.md | 2 +- packages/reqresp/tsconfig.build.json | 12 +++++++++--- packages/reqresp/tsconfig.json | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/reqresp/README.md b/packages/reqresp/README.md index 93c124dc8537..10df94cec9c6 100644 --- a/packages/reqresp/README.md +++ b/packages/reqresp/README.md @@ -18,7 +18,7 @@ import {Ping} from "@lodestar/reqresp/messages"; import {ILogger} from "@lodestar/utils"; async function getReqResp(libp2p: Libp2p, logger: ILogger): Promise { - const reqResp = new ReqResp({libp2p, logger, metrics: null}); + const reqResp = new ReqResp({libp2p, logger, metricsRegister: null}); // Register a PONG handler to respond with caller's Ping request reqResp.registerProtocol( diff --git a/packages/reqresp/tsconfig.build.json b/packages/reqresp/tsconfig.build.json index b46adfa48cb7..3e8b01f280eb 100644 --- a/packages/reqresp/tsconfig.build.json +++ b/packages/reqresp/tsconfig.build.json @@ -1,8 +1,14 @@ { "extends": "../../tsconfig.build.json", - "include": ["src"], + "include": [ + "src" + ], "compilerOptions": { "outDir": "lib", - "typeRoots": ["../../node_modules/@types", "./node_modules/@types", "../../types"] + "typeRoots": [ + "../../node_modules/@types", + "./node_modules/@types", + "../../types" + ] } -} +} \ No newline at end of file diff --git a/packages/reqresp/tsconfig.json b/packages/reqresp/tsconfig.json index 4feb265109be..19a74dd21227 100644 --- a/packages/reqresp/tsconfig.json +++ b/packages/reqresp/tsconfig.json @@ -8,5 +8,5 @@ "../../node_modules/@types", "../../types" ] - }, + } } \ No newline at end of file From 557ba229f1c0dc3f7e9071033718e3ad7b297036 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sun, 20 Nov 2022 02:43:05 +0100 Subject: [PATCH 22/23] Rename class file to its constructor name --- packages/beacon-node/src/network/events.ts | 2 +- packages/beacon-node/src/network/index.ts | 2 +- packages/beacon-node/src/network/interface.ts | 2 +- packages/beacon-node/src/network/network.ts | 2 +- packages/beacon-node/src/network/options.ts | 2 +- .../src/network/peers/peerManager.ts | 2 +- .../src/network/reqresp/ReqRespBeaconNode.ts | 307 +++++++++++++++++ .../beacon-node/src/network/reqresp/index.ts | 309 +----------------- .../e2e/network/peers/peerManager.test.ts | 2 +- 9 files changed, 316 insertions(+), 314 deletions(-) create mode 100644 packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts diff --git a/packages/beacon-node/src/network/events.ts b/packages/beacon-node/src/network/events.ts index 60d072432b1a..4cc23bcab1b2 100644 --- a/packages/beacon-node/src/network/events.ts +++ b/packages/beacon-node/src/network/events.ts @@ -2,7 +2,7 @@ import {EventEmitter} from "events"; import {PeerId} from "@libp2p/interface-peer-id"; import StrictEventEmitter from "strict-event-emitter-types"; import {allForks, phase0} from "@lodestar/types"; -import {RequestTypedContainer} from "./reqresp/index.js"; +import {RequestTypedContainer} from "./reqresp/ReqRespBeaconNode.js"; export enum NetworkEvent { /** A relevant peer has connected or has been re-STATUS'd */ diff --git a/packages/beacon-node/src/network/index.ts b/packages/beacon-node/src/network/index.ts index 398b75fe61f0..491deb57fb5e 100644 --- a/packages/beacon-node/src/network/index.ts +++ b/packages/beacon-node/src/network/index.ts @@ -3,6 +3,6 @@ export * from "./interface.js"; export * from "./network.js"; export * from "./nodejs/index.js"; export * from "./gossip/index.js"; -export * from "./reqresp/index.js"; +export * from "./reqresp/ReqRespBeaconNode.js"; export * from "./util.js"; export * from "./peers/index.js"; diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index ff6cc862cb32..2f41efc94de0 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -6,7 +6,7 @@ import {INetworkEventBus} from "./events.js"; import {Eth2Gossipsub} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {PeerAction} from "./peers/index.js"; -import {IReqRespBeaconNode} from "./reqresp/index.js"; +import {IReqRespBeaconNode} from "./reqresp/ReqRespBeaconNode.js"; import {IAttnetsService, ISubnetsService, CommitteeSubscription} from "./subnets/index.js"; export type PeerSearchOptions = { diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 6067e348024c..12577336a7f1 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -13,7 +13,7 @@ import {IMetrics} from "../metrics/index.js"; import {ChainEvent, IBeaconChain, IBeaconClock} from "../chain/index.js"; import {INetworkOptions} from "./options.js"; import {INetwork} from "./interface.js"; -import {IReqRespBeaconNode, ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/index.js"; +import {IReqRespBeaconNode, ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/ReqRespBeaconNode.js"; import {Eth2Gossipsub, getGossipHandlers, GossipHandlers, GossipType} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {FORK_EPOCH_LOOKAHEAD, getActiveForks} from "./forks.js"; diff --git a/packages/beacon-node/src/network/options.ts b/packages/beacon-node/src/network/options.ts index 64daf63730be..5d561bb67cf9 100644 --- a/packages/beacon-node/src/network/options.ts +++ b/packages/beacon-node/src/network/options.ts @@ -2,7 +2,7 @@ import {ENR, IDiscv5DiscoveryInputOptions} from "@chainsafe/discv5"; import {Eth2GossipsubOpts} from "./gossip/gossipsub.js"; import {defaultGossipHandlerOpts, GossipHandlerOpts} from "./gossip/handlers/index.js"; import {PeerManagerOpts} from "./peers/index.js"; -import {ReqRespBeaconNodeOpts} from "./reqresp/index.js"; +import {ReqRespBeaconNodeOpts} from "./reqresp/ReqRespBeaconNode.js"; export interface INetworkOptions extends PeerManagerOpts, ReqRespBeaconNodeOpts, GossipHandlerOpts, Eth2GossipsubOpts { localMultiaddrs: string[]; diff --git a/packages/beacon-node/src/network/peers/peerManager.ts b/packages/beacon-node/src/network/peers/peerManager.ts index adea2377baf6..a5f091134ad9 100644 --- a/packages/beacon-node/src/network/peers/peerManager.ts +++ b/packages/beacon-node/src/network/peers/peerManager.ts @@ -11,7 +11,7 @@ import {IBeaconChain} from "../../chain/index.js"; import {GoodByeReasonCode, GOODBYE_KNOWN_CODES, Libp2pEvent} from "../../constants/index.js"; import {IMetrics} from "../../metrics/index.js"; import {NetworkEvent, INetworkEventBus} from "../events.js"; -import {IReqRespBeaconNode, ReqRespMethod, RequestTypedContainer} from "../reqresp/index.js"; +import {IReqRespBeaconNode, ReqRespMethod, RequestTypedContainer} from "../reqresp/ReqRespBeaconNode.js"; import {getConnection, getConnectionsMap, prettyPrintPeerId} from "../util.js"; import {ISubnetsService} from "../subnets/index.js"; import {SubnetType} from "../metadata.js"; diff --git a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts new file mode 100644 index 000000000000..28ec907185fb --- /dev/null +++ b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts @@ -0,0 +1,307 @@ +import {Libp2p} from "libp2p"; +import {PeerId} from "@libp2p/interface-peer-id"; +import {ForkName} from "@lodestar/params"; +import {ILogger} from "@lodestar/utils"; +import {IBeaconConfig} from "@lodestar/config"; +import {ReqRespOpts} from "@lodestar/reqresp/lib/ReqResp.js"; + +import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; +import { + collectExactOne, + collectMaxResponse, + EncodedPayload, + EncodedPayloadType, + Encoding, + ProtocolDefinition, + ReqResp, + RequestError, + ResponseError, +} from "@lodestar/reqresp"; +import * as messages from "@lodestar/reqresp/messages"; +import {IMetrics} from "../../metrics/metrics.js"; +import {INetworkEventBus, NetworkEvent} from "../events.js"; +import {IPeerRpcScoreStore, PeerAction} from "../peers/score.js"; +import {MetadataController} from "../metadata.js"; +import {PeersData} from "../peers/peersData.js"; +import {ReqRespHandlers} from "./handlers/index.js"; +import {collectSequentialBlocksInRange} from "./utils/collectSequentialBlocksInRange.js"; +import {IReqRespBeaconNode, RespStatus} from "./interface.js"; +import {ReqRespMethod, RequestTypedContainer, Version} from "./types.js"; +import {onOutgoingReqRespError} from "./score.js"; +import {InboundRateLimiter, RateLimiterOptions} from "./inboundRateLimiter.js"; + +export {IReqRespBeaconNode}; +export {ReqRespMethod, RequestTypedContainer} from "./types.js"; +export {getReqRespHandlers, ReqRespHandlers} from "./handlers/index.js"; + +/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ +export type ReqRespBlockResponse = EncodedPayload; + +export interface ReqRespBeaconNodeModules { + libp2p: Libp2p; + peersData: PeersData; + logger: ILogger; + config: IBeaconConfig; + metrics: IMetrics | null; + reqRespHandlers: ReqRespHandlers; + metadata: MetadataController; + peerRpcScores: IPeerRpcScoreStore; + networkEventBus: INetworkEventBus; +} + +export interface ReqRespBeaconNodeOpts extends ReqRespOpts, RateLimiterOptions { + /** maximum request count we can serve per peer within rateTrackerTimeoutMs */ + requestCountPeerLimit?: number; + /** maximum block count we can serve per peer within rateTrackerTimeoutMs */ + blockCountPeerLimit?: number; + /** maximum block count we can serve for all peers within rateTrackerTimeoutMs */ + blockCountTotalLimit?: number; + /** the time period we want to track total requests or objects, normally 1 min */ + rateTrackerTimeoutMs?: number; +} + +/** + * Implementation of Ethereum Consensus p2p Req/Resp domain. + * For the spec that this code is based on, see: + * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain + * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain + */ +export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { + private readonly reqRespHandlers: ReqRespHandlers; + private readonly metadataController: MetadataController; + private readonly peerRpcScores: IPeerRpcScoreStore; + private readonly inboundRateLimiter: InboundRateLimiter; + private readonly networkEventBus: INetworkEventBus; + private readonly peersData: PeersData; + + constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) { + const {reqRespHandlers, networkEventBus, peersData, peerRpcScores, metadata, logger, metrics} = modules; + + super({...modules, metricsRegister: metrics?.register ?? null}, options); + + this.reqRespHandlers = reqRespHandlers; + this.peerRpcScores = peerRpcScores; + this.peersData = peersData; + this.metadataController = metadata; + this.networkEventBus = networkEventBus; + this.inboundRateLimiter = new InboundRateLimiter(options, { + logger, + reportPeer: (peerId) => peerRpcScores.applyAction(peerId, PeerAction.Fatal, "rate_limit_rpc"), + metrics, + }); + + // TODO: Do not register everything! Some protocols are fork dependant + this.registerProtocol(messages.Ping(this.onPing.bind(this))); + this.registerProtocol(messages.Status(modules, this.onStatus.bind(this))); + this.registerProtocol(messages.Metadata(modules, this.onMetadata.bind(this))); + this.registerProtocol(messages.MetadataV2(modules, this.onMetadata.bind(this))); + this.registerProtocol(messages.Goodbye(modules, this.onGoodbye.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRange(modules, this.onBeaconBlocksByRange.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRangeV2(modules, this.onBeaconBlocksByRange.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRoot(modules, this.onBeaconBlocksByRoot.bind(this))); + this.registerProtocol(messages.BeaconBlocksByRootV2(modules, this.onBeaconBlocksByRoot.bind(this))); + this.registerProtocol(messages.LightClientBootstrap(modules, reqRespHandlers.onLightClientBootstrap)); + this.registerProtocol(messages.LightClientFinalityUpdate(modules, reqRespHandlers.onLightClientFinalityUpdate)); + this.registerProtocol(messages.LightClientOptimisticUpdate(modules, reqRespHandlers.onLightClientOptimisticUpdate)); + this.registerProtocol(messages.LightClientUpdatesByRange(modules, reqRespHandlers.onLightClientUpdatesByRange)); + } + + async start(): Promise { + await super.start(); + this.inboundRateLimiter.start(); + } + + async stop(): Promise { + await super.stop(); + this.inboundRateLimiter.stop(); + } + + pruneOnPeerDisconnect(peerId: PeerId): void { + this.inboundRateLimiter.prune(peerId); + } + + async status(peerId: PeerId, request: phase0.Status): Promise { + return collectExactOne( + this.sendRequest(peerId, ReqRespMethod.Status, [Version.V1], request) + ); + } + + async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { + // TODO: Replace with "ignore response after request" + await collectExactOne( + this.sendRequest(peerId, ReqRespMethod.Goodbye, [Version.V1], request) + ); + } + + async ping(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest( + peerId, + ReqRespMethod.Ping, + [Version.V1], + this.metadataController.seqNumber + ) + ); + } + + async metadata(peerId: PeerId, fork?: ForkName): Promise { + // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version + const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; + return collectExactOne(this.sendRequest(peerId, ReqRespMethod.Metadata, versions, null)); + } + + async beaconBlocksByRange( + peerId: PeerId, + request: phase0.BeaconBlocksByRangeRequest + ): Promise { + return collectSequentialBlocksInRange( + this.sendRequest( + peerId, + ReqRespMethod.BeaconBlocksByRange, + [Version.V2, Version.V1], // Prioritize V2 + request + ), + request + ); + } + + async beaconBlocksByRoot( + peerId: PeerId, + request: phase0.BeaconBlocksByRootRequest + ): Promise { + return collectMaxResponse( + this.sendRequest( + peerId, + ReqRespMethod.BeaconBlocksByRoot, + [Version.V2, Version.V1], // Prioritize V2 + request + ), + request.length + ); + } + + async lightClientBootstrap(peerId: PeerId, request: Root): Promise { + return collectExactOne( + this.sendRequest( + peerId, + ReqRespMethod.LightClientBootstrap, + [Version.V1], + request + ) + ); + } + + async lightClientOptimisticUpdate(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest( + peerId, + ReqRespMethod.LightClientOptimisticUpdate, + [Version.V1], + null + ) + ); + } + + async lightClientFinalityUpdate(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest( + peerId, + ReqRespMethod.LightClientFinalityUpdate, + [Version.V1], + null + ) + ); + } + + async lightClientUpdatesByRange( + peerId: PeerId, + request: altair.LightClientUpdatesByRange + ): Promise { + return collectMaxResponse( + this.sendRequest( + peerId, + ReqRespMethod.LightClientUpdatesByRange, + [Version.V1], + request + ), + request.count + ); + } + + protected sendRequest(peerId: PeerId, method: string, versions: number[], body: Req): AsyncIterable { + // Remember prefered encoding + const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; + + return super.sendRequest(peerId, method, versions, encoding, body); + } + + protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { + // Allow onRequest to return and close the stream + // For Goodbye there may be a race condition where the listener of `receivedGoodbye` + // disconnects in the same syncronous call, preventing the stream from ending cleanly + setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); + } + + protected onIncomingRequest(peerId: PeerId, protocol: ProtocolDefinition): void { + if (protocol.method !== ReqRespMethod.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + + // Remember prefered encoding + if (protocol.method === ReqRespMethod.Status) { + this.peersData.setEncodingPreference(peerId.toString(), protocol.encoding); + } + } + + protected onOutgoingRequestError(peerId: PeerId, method: ReqRespMethod, error: RequestError): void { + const peerAction = onOutgoingReqRespError(error, method); + if (peerAction !== null) { + this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); + } + } + + private async *onStatus(req: phase0.Status, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: ReqRespMethod.Status, body: req}, peerId); + + yield* this.reqRespHandlers.onStatus(req, peerId); + } + + private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body: req}, peerId); + + yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; + } + + private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: ReqRespMethod.Ping, body: req}, peerId); + yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; + } + + private async *onMetadata(req: null, peerId: PeerId): AsyncIterable> { + this.onIncomingRequestBody({method: ReqRespMethod.Metadata, body: req}, peerId); + + // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property + // It's safe to return altair.Metadata here for all versions + yield {type: EncodedPayloadType.ssz, data: this.metadataController.json}; + } + + private async *onBeaconBlocksByRange( + req: phase0.BeaconBlocksByRangeRequest, + peerId: PeerId + ): AsyncIterable> { + if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + yield* this.reqRespHandlers.onBeaconBlocksByRange(req, peerId); + } + + private async *onBeaconBlocksByRoot( + req: phase0.BeaconBlocksByRootRequest, + peerId: PeerId + ): AsyncIterable> { + if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { + throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); + } + yield* this.reqRespHandlers.onBeaconBlocksByRoot(req, peerId); + } +} diff --git a/packages/beacon-node/src/network/reqresp/index.ts b/packages/beacon-node/src/network/reqresp/index.ts index 28ec907185fb..033834c4eadf 100644 --- a/packages/beacon-node/src/network/reqresp/index.ts +++ b/packages/beacon-node/src/network/reqresp/index.ts @@ -1,307 +1,2 @@ -import {Libp2p} from "libp2p"; -import {PeerId} from "@libp2p/interface-peer-id"; -import {ForkName} from "@lodestar/params"; -import {ILogger} from "@lodestar/utils"; -import {IBeaconConfig} from "@lodestar/config"; -import {ReqRespOpts} from "@lodestar/reqresp/lib/ReqResp.js"; - -import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; -import { - collectExactOne, - collectMaxResponse, - EncodedPayload, - EncodedPayloadType, - Encoding, - ProtocolDefinition, - ReqResp, - RequestError, - ResponseError, -} from "@lodestar/reqresp"; -import * as messages from "@lodestar/reqresp/messages"; -import {IMetrics} from "../../metrics/metrics.js"; -import {INetworkEventBus, NetworkEvent} from "../events.js"; -import {IPeerRpcScoreStore, PeerAction} from "../peers/score.js"; -import {MetadataController} from "../metadata.js"; -import {PeersData} from "../peers/peersData.js"; -import {ReqRespHandlers} from "./handlers/index.js"; -import {collectSequentialBlocksInRange} from "./utils/collectSequentialBlocksInRange.js"; -import {IReqRespBeaconNode, RespStatus} from "./interface.js"; -import {ReqRespMethod, RequestTypedContainer, Version} from "./types.js"; -import {onOutgoingReqRespError} from "./score.js"; -import {InboundRateLimiter, RateLimiterOptions} from "./inboundRateLimiter.js"; - -export {IReqRespBeaconNode}; -export {ReqRespMethod, RequestTypedContainer} from "./types.js"; -export {getReqRespHandlers, ReqRespHandlers} from "./handlers/index.js"; - -/** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ -export type ReqRespBlockResponse = EncodedPayload; - -export interface ReqRespBeaconNodeModules { - libp2p: Libp2p; - peersData: PeersData; - logger: ILogger; - config: IBeaconConfig; - metrics: IMetrics | null; - reqRespHandlers: ReqRespHandlers; - metadata: MetadataController; - peerRpcScores: IPeerRpcScoreStore; - networkEventBus: INetworkEventBus; -} - -export interface ReqRespBeaconNodeOpts extends ReqRespOpts, RateLimiterOptions { - /** maximum request count we can serve per peer within rateTrackerTimeoutMs */ - requestCountPeerLimit?: number; - /** maximum block count we can serve per peer within rateTrackerTimeoutMs */ - blockCountPeerLimit?: number; - /** maximum block count we can serve for all peers within rateTrackerTimeoutMs */ - blockCountTotalLimit?: number; - /** the time period we want to track total requests or objects, normally 1 min */ - rateTrackerTimeoutMs?: number; -} - -/** - * Implementation of Ethereum Consensus p2p Req/Resp domain. - * For the spec that this code is based on, see: - * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain - * https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain - */ -export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { - private readonly reqRespHandlers: ReqRespHandlers; - private readonly metadataController: MetadataController; - private readonly peerRpcScores: IPeerRpcScoreStore; - private readonly inboundRateLimiter: InboundRateLimiter; - private readonly networkEventBus: INetworkEventBus; - private readonly peersData: PeersData; - - constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) { - const {reqRespHandlers, networkEventBus, peersData, peerRpcScores, metadata, logger, metrics} = modules; - - super({...modules, metricsRegister: metrics?.register ?? null}, options); - - this.reqRespHandlers = reqRespHandlers; - this.peerRpcScores = peerRpcScores; - this.peersData = peersData; - this.metadataController = metadata; - this.networkEventBus = networkEventBus; - this.inboundRateLimiter = new InboundRateLimiter(options, { - logger, - reportPeer: (peerId) => peerRpcScores.applyAction(peerId, PeerAction.Fatal, "rate_limit_rpc"), - metrics, - }); - - // TODO: Do not register everything! Some protocols are fork dependant - this.registerProtocol(messages.Ping(this.onPing.bind(this))); - this.registerProtocol(messages.Status(modules, this.onStatus.bind(this))); - this.registerProtocol(messages.Metadata(modules, this.onMetadata.bind(this))); - this.registerProtocol(messages.MetadataV2(modules, this.onMetadata.bind(this))); - this.registerProtocol(messages.Goodbye(modules, this.onGoodbye.bind(this))); - this.registerProtocol(messages.BeaconBlocksByRange(modules, this.onBeaconBlocksByRange.bind(this))); - this.registerProtocol(messages.BeaconBlocksByRangeV2(modules, this.onBeaconBlocksByRange.bind(this))); - this.registerProtocol(messages.BeaconBlocksByRoot(modules, this.onBeaconBlocksByRoot.bind(this))); - this.registerProtocol(messages.BeaconBlocksByRootV2(modules, this.onBeaconBlocksByRoot.bind(this))); - this.registerProtocol(messages.LightClientBootstrap(modules, reqRespHandlers.onLightClientBootstrap)); - this.registerProtocol(messages.LightClientFinalityUpdate(modules, reqRespHandlers.onLightClientFinalityUpdate)); - this.registerProtocol(messages.LightClientOptimisticUpdate(modules, reqRespHandlers.onLightClientOptimisticUpdate)); - this.registerProtocol(messages.LightClientUpdatesByRange(modules, reqRespHandlers.onLightClientUpdatesByRange)); - } - - async start(): Promise { - await super.start(); - this.inboundRateLimiter.start(); - } - - async stop(): Promise { - await super.stop(); - this.inboundRateLimiter.stop(); - } - - pruneOnPeerDisconnect(peerId: PeerId): void { - this.inboundRateLimiter.prune(peerId); - } - - async status(peerId: PeerId, request: phase0.Status): Promise { - return collectExactOne( - this.sendRequest(peerId, ReqRespMethod.Status, [Version.V1], request) - ); - } - - async goodbye(peerId: PeerId, request: phase0.Goodbye): Promise { - // TODO: Replace with "ignore response after request" - await collectExactOne( - this.sendRequest(peerId, ReqRespMethod.Goodbye, [Version.V1], request) - ); - } - - async ping(peerId: PeerId): Promise { - return collectExactOne( - this.sendRequest( - peerId, - ReqRespMethod.Ping, - [Version.V1], - this.metadataController.seqNumber - ) - ); - } - - async metadata(peerId: PeerId, fork?: ForkName): Promise { - // Only request V1 if forcing phase0 fork. It's safe to not specify `fork` and let stream negotiation pick the version - const versions = fork === ForkName.phase0 ? [Version.V1] : [Version.V2, Version.V1]; - return collectExactOne(this.sendRequest(peerId, ReqRespMethod.Metadata, versions, null)); - } - - async beaconBlocksByRange( - peerId: PeerId, - request: phase0.BeaconBlocksByRangeRequest - ): Promise { - return collectSequentialBlocksInRange( - this.sendRequest( - peerId, - ReqRespMethod.BeaconBlocksByRange, - [Version.V2, Version.V1], // Prioritize V2 - request - ), - request - ); - } - - async beaconBlocksByRoot( - peerId: PeerId, - request: phase0.BeaconBlocksByRootRequest - ): Promise { - return collectMaxResponse( - this.sendRequest( - peerId, - ReqRespMethod.BeaconBlocksByRoot, - [Version.V2, Version.V1], // Prioritize V2 - request - ), - request.length - ); - } - - async lightClientBootstrap(peerId: PeerId, request: Root): Promise { - return collectExactOne( - this.sendRequest( - peerId, - ReqRespMethod.LightClientBootstrap, - [Version.V1], - request - ) - ); - } - - async lightClientOptimisticUpdate(peerId: PeerId): Promise { - return collectExactOne( - this.sendRequest( - peerId, - ReqRespMethod.LightClientOptimisticUpdate, - [Version.V1], - null - ) - ); - } - - async lightClientFinalityUpdate(peerId: PeerId): Promise { - return collectExactOne( - this.sendRequest( - peerId, - ReqRespMethod.LightClientFinalityUpdate, - [Version.V1], - null - ) - ); - } - - async lightClientUpdatesByRange( - peerId: PeerId, - request: altair.LightClientUpdatesByRange - ): Promise { - return collectMaxResponse( - this.sendRequest( - peerId, - ReqRespMethod.LightClientUpdatesByRange, - [Version.V1], - request - ), - request.count - ); - } - - protected sendRequest(peerId: PeerId, method: string, versions: number[], body: Req): AsyncIterable { - // Remember prefered encoding - const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; - - return super.sendRequest(peerId, method, versions, encoding, body); - } - - protected onIncomingRequestBody(req: RequestTypedContainer, peerId: PeerId): void { - // Allow onRequest to return and close the stream - // For Goodbye there may be a race condition where the listener of `receivedGoodbye` - // disconnects in the same syncronous call, preventing the stream from ending cleanly - setTimeout(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, req, peerId), 0); - } - - protected onIncomingRequest(peerId: PeerId, protocol: ProtocolDefinition): void { - if (protocol.method !== ReqRespMethod.Goodbye && !this.inboundRateLimiter.allowRequest(peerId)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - - // Remember prefered encoding - if (protocol.method === ReqRespMethod.Status) { - this.peersData.setEncodingPreference(peerId.toString(), protocol.encoding); - } - } - - protected onOutgoingRequestError(peerId: PeerId, method: ReqRespMethod, error: RequestError): void { - const peerAction = onOutgoingReqRespError(error, method); - if (peerAction !== null) { - this.peerRpcScores.applyAction(peerId, peerAction, error.type.code); - } - } - - private async *onStatus(req: phase0.Status, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: ReqRespMethod.Status, body: req}, peerId); - - yield* this.reqRespHandlers.onStatus(req, peerId); - } - - private async *onGoodbye(req: phase0.Goodbye, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body: req}, peerId); - - yield {type: EncodedPayloadType.ssz, data: BigInt(0)}; - } - - private async *onPing(req: phase0.Ping, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: ReqRespMethod.Ping, body: req}, peerId); - yield {type: EncodedPayloadType.ssz, data: this.metadataController.seqNumber}; - } - - private async *onMetadata(req: null, peerId: PeerId): AsyncIterable> { - this.onIncomingRequestBody({method: ReqRespMethod.Metadata, body: req}, peerId); - - // V1 -> phase0, V2 -> altair. But the type serialization of phase0.Metadata will just ignore the extra .syncnets property - // It's safe to return altair.Metadata here for all versions - yield {type: EncodedPayloadType.ssz, data: this.metadataController.json}; - } - - private async *onBeaconBlocksByRange( - req: phase0.BeaconBlocksByRangeRequest, - peerId: PeerId - ): AsyncIterable> { - if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.count)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - yield* this.reqRespHandlers.onBeaconBlocksByRange(req, peerId); - } - - private async *onBeaconBlocksByRoot( - req: phase0.BeaconBlocksByRootRequest, - peerId: PeerId - ): AsyncIterable> { - if (!this.inboundRateLimiter.allowBlockByRequest(peerId, req.length)) { - throw new ResponseError(RespStatus.RATE_LIMITED, "rate limit"); - } - yield* this.reqRespHandlers.onBeaconBlocksByRoot(req, peerId); - } -} +export * from "./ReqRespBeaconNode.js"; +export * from "./interface.js"; diff --git a/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts b/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts index d5dba11f5018..9af560172b81 100644 --- a/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts +++ b/packages/beacon-node/test/e2e/network/peers/peerManager.test.ts @@ -8,7 +8,7 @@ import {BitArray} from "@chainsafe/ssz"; import {altair, phase0, ssz} from "@lodestar/types"; import {sleep} from "@lodestar/utils"; import {createIBeaconConfig} from "@lodestar/config"; -import {IReqRespBeaconNode, ReqRespMethod} from "../../../../src/network/reqresp/index.js"; +import {IReqRespBeaconNode, ReqRespMethod} from "../../../../src/network/reqresp/ReqRespBeaconNode.js"; import {PeerRpcScoreStore, PeerManager} from "../../../../src/network/peers/index.js"; import {Eth2Gossipsub, getConnectionsMap, NetworkEvent, NetworkEventBus} from "../../../../src/network/index.js"; import {PeersData} from "../../../../src/network/peers/peersData.js"; From 1b81d7273339e40ea270a63acfc4c6e700e0b3e9 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sun, 20 Nov 2022 02:56:18 +0100 Subject: [PATCH 23/23] Fix some linter errors --- .../src/network/reqresp/ReqRespBeaconNode.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts index 28ec907185fb..0e0b9917eb78 100644 --- a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts +++ b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts @@ -1,11 +1,7 @@ -import {Libp2p} from "libp2p"; import {PeerId} from "@libp2p/interface-peer-id"; -import {ForkName} from "@lodestar/params"; -import {ILogger} from "@lodestar/utils"; +import {Libp2p} from "libp2p"; import {IBeaconConfig} from "@lodestar/config"; -import {ReqRespOpts} from "@lodestar/reqresp/lib/ReqResp.js"; - -import {allForks, altair, phase0, Root, Slot} from "@lodestar/types"; +import {ForkName} from "@lodestar/params"; import { collectExactOne, collectMaxResponse, @@ -17,22 +13,25 @@ import { RequestError, ResponseError, } from "@lodestar/reqresp"; +import {ReqRespOpts} from "@lodestar/reqresp/lib/ReqResp.js"; import * as messages from "@lodestar/reqresp/messages"; +import {allForks, altair, phase0, Root} from "@lodestar/types"; +import {ILogger} from "@lodestar/utils"; import {IMetrics} from "../../metrics/metrics.js"; import {INetworkEventBus, NetworkEvent} from "../events.js"; -import {IPeerRpcScoreStore, PeerAction} from "../peers/score.js"; import {MetadataController} from "../metadata.js"; import {PeersData} from "../peers/peersData.js"; +import {IPeerRpcScoreStore, PeerAction} from "../peers/score.js"; import {ReqRespHandlers} from "./handlers/index.js"; -import {collectSequentialBlocksInRange} from "./utils/collectSequentialBlocksInRange.js"; +import {InboundRateLimiter, RateLimiterOptions} from "./inboundRateLimiter.js"; import {IReqRespBeaconNode, RespStatus} from "./interface.js"; -import {ReqRespMethod, RequestTypedContainer, Version} from "./types.js"; import {onOutgoingReqRespError} from "./score.js"; -import {InboundRateLimiter, RateLimiterOptions} from "./inboundRateLimiter.js"; +import {ReqRespMethod, RequestTypedContainer, Version} from "./types.js"; +import {collectSequentialBlocksInRange} from "./utils/collectSequentialBlocksInRange.js"; -export {IReqRespBeaconNode}; -export {ReqRespMethod, RequestTypedContainer} from "./types.js"; export {getReqRespHandlers, ReqRespHandlers} from "./handlers/index.js"; +export {ReqRespMethod, RequestTypedContainer} from "./types.js"; +export {IReqRespBeaconNode}; /** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ export type ReqRespBlockResponse = EncodedPayload;