UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

390 lines (346 loc) • 15.5 kB
import {PeerId} from "@libp2p/interface"; import {Libp2p} from "libp2p"; import {BeaconConfig, ForkBoundary} from "@lodestar/config"; import {ForkName, ForkSeq, GENESIS_EPOCH} from "@lodestar/params"; import { Encoding, ProtocolDescriptor, ProtocolHandler, ReqResp, ReqRespOpts, ReqRespRequest, RequestError, ResponseIncoming, ResponseOutgoing, } from "@lodestar/reqresp"; import {Metadata, Status, phase0, ssz} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {NetworkCoreMetrics} from "../core/metrics.js"; import {INetworkEventBus, NetworkEvent} from "../events.js"; import {MetadataController} from "../metadata.js"; import {ClientKind} from "../peers/client.js"; import {PeersData} from "../peers/peersData.js"; import {IPeerRpcScoreStore, PeerAction} from "../peers/score/index.js"; import {StatusCache} from "../statusCache.js"; import * as protocols from "./protocols.js"; import {onOutgoingReqRespError} from "./score.js"; import { GetReqRespHandlerFn, ProtocolNoHandler, ReqRespMethod, RequestTypedContainer, Version, requestSszTypeByMethod, responseSszTypeByMethod, } from "./types.js"; import {collectExactOneTyped} from "./utils/collect.js"; export {getReqRespHandlers} from "./handlers/index.js"; export {ReqRespMethod, type RequestTypedContainer} from "./types.js"; export interface ReqRespBeaconNodeModules { libp2p: Libp2p; peersData: PeersData; logger: Logger; config: BeaconConfig; metrics: NetworkCoreMetrics | null; metadata: MetadataController; peerRpcScores: IPeerRpcScoreStore; events: INetworkEventBus; statusCache: StatusCache; getHandler: GetReqRespHandlerFn; } export type ReqRespBeaconNodeOpts = ReqRespOpts & {disableLightClientServer?: boolean}; /** * 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/v1.6.1/specs/altair/light-client/p2p-interface.md#the-reqresp-domain */ export class ReqRespBeaconNode extends ReqResp { private readonly metadataController: MetadataController; private readonly peerRpcScores: IPeerRpcScoreStore; private readonly networkEventBus: INetworkEventBus; private readonly peersData: PeersData; private readonly statusCache: StatusCache; private readonly getHandler: GetReqRespHandlerFn; /** Track registered fork to only send to known protocols */ private currentRegisteredFork: ForkSeq = ForkSeq.phase0; private readonly config: BeaconConfig; protected readonly logger: Logger; protected readonly disableLightClientServer: boolean; constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) { const {events, peersData, peerRpcScores, metadata, metrics, logger} = modules; super( { ...modules, metricsRegister: metrics?.register ?? null, }, { ...options, onRateLimit(peerId, method) { logger.debug("Do not serve request due to rate limit", {peerId: peerId.toString()}); peerRpcScores.applyAction(peerId, PeerAction.Fatal, "rate_limit_rpc"); metrics?.reqResp.rateLimitErrors.inc({method}); }, getPeerLogMetadata(peerId) { // this logs the whole agent version for unknown client which is good for debugging return peersData.getPeerKind(peerId) ?? peersData.getAgentVersion(peerId); }, } ); this.disableLightClientServer = options.disableLightClientServer ?? false; this.peerRpcScores = peerRpcScores; this.peersData = peersData; this.config = modules.config; this.logger = logger; this.metadataController = metadata; this.networkEventBus = events; this.statusCache = modules.statusCache; this.getHandler = modules.getHandler; } async start(): Promise<void> { await super.start(); } async stop(): Promise<void> { await super.stop(); } // NOTE: Do not pruneOnPeerDisconnect. Persist peer rate limit data until pruned by time // pruneOnPeerDisconnect(peerId: PeerId): void { // this.rateLimiter.prune(peerId); registerProtocolsAtBoundary(boundary: ForkBoundary): void { this.currentRegisteredFork = ForkSeq[boundary.fork]; const mustSubscribeProtocols = this.getProtocolsAtBoundary(boundary); const mustSubscribeProtocolIDs = new Set( mustSubscribeProtocols.map(([protocol]) => this.formatProtocolID(protocol)) ); // Un-subscribe not required protocols for (const protocolID of this.getRegisteredProtocols()) { if (!mustSubscribeProtocolIDs.has(protocolID)) { // Async because of writing to peerstore -_- should never throw this.unregisterProtocol(protocolID).catch((e) => { this.logger.error("Error on ReqResp.unregisterProtocol", {protocolID}, e); }); } } // Subscribe required protocols for (const [protocol, handler] of mustSubscribeProtocols) { this.registerProtocol({...protocol, handler}).catch((e) => { this.logger.error("Error on ReqResp.registerProtocol", {protocolID: this.formatProtocolID(protocol)}, e); }); } } sendRequestWithoutEncoding( peerId: PeerId, method: ReqRespMethod, versions: number[], requestData: Uint8Array ): AsyncIterable<ResponseIncoming> { // Remember preferred encoding const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; // Overwrite placeholder requestData from main thread with correct sequenceNumber if (method === ReqRespMethod.Ping) { requestData = requestSszTypeByMethod(ForkName.phase0, this.config)[ReqRespMethod.Ping].serialize( this.metadataController.seqNumber ); } // ReqResp outgoing request, emit from main thread to worker return this.sendRequest(peerId, method, versions, encoding, requestData); } async sendPing(peerId: PeerId): Promise<phase0.Ping> { return collectExactOneTyped( // Ping method request data is overwritten in worker with correct sequence number this.sendReqRespRequest(peerId, ReqRespMethod.Ping, [Version.V1], this.metadataController.seqNumber), responseSszTypeByMethod[ReqRespMethod.Ping] ); } async sendStatus(peerId: PeerId, request: Status): Promise<Status> { return collectExactOneTyped( this.sendReqRespRequest( peerId, ReqRespMethod.Status, this.currentRegisteredFork >= ForkSeq.fulu ? [Version.V2] : [Version.V1], request ), responseSszTypeByMethod[ReqRespMethod.Status] ); } async sendGoodbye(peerId: PeerId, request: phase0.Goodbye): Promise<void> { // TODO: Replace with "ignore response after request" await collectExactOneTyped( this.sendReqRespRequest(peerId, ReqRespMethod.Goodbye, [Version.V1], request), responseSszTypeByMethod[ReqRespMethod.Goodbye] ); } async sendMetadata(peerId: PeerId): Promise<Metadata> { return collectExactOneTyped( this.sendReqRespRequest( peerId, ReqRespMethod.Metadata, this.currentRegisteredFork >= ForkSeq.fulu ? [Version.V3] : this.currentRegisteredFork >= ForkSeq.altair ? [Version.V3, Version.V2] : [Version.V2, Version.V1], null ), responseSszTypeByMethod[ReqRespMethod.Metadata] ); } private sendReqRespRequest<Req>( peerId: PeerId, method: ReqRespMethod, versions: number[], request: Req ): AsyncIterable<ResponseIncoming> { const fork = ForkName[ForkSeq[this.currentRegisteredFork] as ForkName]; const requestType = requestSszTypeByMethod(fork, this.config)[method]; const requestData = requestType ? requestType.serialize(request as never) : new Uint8Array(); return this.sendRequestWithoutEncoding(peerId, method, versions, requestData); } /** * Returns the list of protocols that must be subscribed during a specific fork. * Any protocol not in this list must be un-subscribed. */ private getProtocolsAtBoundary(boundary: ForkBoundary): [ProtocolNoHandler, ProtocolHandler][] { const {fork} = boundary; const protocolsAtFork: [ProtocolNoHandler, ProtocolHandler][] = [ [protocols.Ping(fork, this.config), this.onPing.bind(this)], [protocols.Goodbye(fork, this.config), this.onGoodbye.bind(this)], // Support V3 methods as soon as implemented (for fulu) // Follows pattern for altair: // Ref https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/p2p-interface.md#transitioning-from-v1-to-v2 [protocols.MetadataV3(fork, this.config), this.onMetadata.bind(this)], [protocols.BeaconBlocksByRangeV2(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByRange)], [protocols.BeaconBlocksByRootV2(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByRoot)], ]; if (ForkSeq[fork] < ForkSeq.altair) { // Unregister V1 topics at the fork boundary, so only declare for pre-altair protocolsAtFork.push( [protocols.Metadata(fork, this.config), this.onMetadata.bind(this)], [protocols.BeaconBlocksByRange(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByRange)], [protocols.BeaconBlocksByRoot(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByRoot)] ); } if (ForkSeq[fork] >= ForkSeq.altair && !this.disableLightClientServer) { // Should be okay to enable before altair, but for consistency only enable afterwards protocolsAtFork.push( [protocols.LightClientBootstrap(fork, this.config), this.getHandler(ReqRespMethod.LightClientBootstrap)], [ protocols.LightClientFinalityUpdate(fork, this.config), this.getHandler(ReqRespMethod.LightClientFinalityUpdate), ], [ protocols.LightClientOptimisticUpdate(fork, this.config), this.getHandler(ReqRespMethod.LightClientOptimisticUpdate), ], [ protocols.LightClientUpdatesByRange(fork, this.config), this.getHandler(ReqRespMethod.LightClientUpdatesByRange), ] ); } if (ForkSeq[fork] >= ForkSeq.deneb) { protocolsAtFork.push( [protocols.BlobSidecarsByRoot(fork, this.config), this.getHandler(ReqRespMethod.BlobSidecarsByRoot)], [protocols.BlobSidecarsByRange(fork, this.config), this.getHandler(ReqRespMethod.BlobSidecarsByRange)] ); } if (ForkSeq[fork] < ForkSeq.fulu) { // Unregister StatusV1, MetadataV2 at the fork boundary, so only declare for pre-fulu protocolsAtFork.push( [protocols.Status(fork, this.config), this.onStatus.bind(this)], [protocols.MetadataV2(fork, this.config), this.onMetadata.bind(this)] ); } else { protocolsAtFork.push( // We can't handle StatusV2 correctly pre-fulu as request type is selected based on fork // instead of protocol version. This is not easily fixable with our current architecture. // See https://github.com/ChainSafe/lodestar/pull/8168 for more details. [protocols.StatusV2(fork, this.config), this.onStatus.bind(this)], [protocols.BeaconBlocksByHead(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByHead)], [ protocols.DataColumnSidecarsByRoot(fork, this.config), this.getHandler(ReqRespMethod.DataColumnSidecarsByRoot), ], [ protocols.DataColumnSidecarsByRange(fork, this.config), this.getHandler(ReqRespMethod.DataColumnSidecarsByRange), ] ); } if (ForkSeq[fork] >= ForkSeq.gloas) { protocolsAtFork.push( [ protocols.ExecutionPayloadEnvelopesByRoot(fork, this.config), this.getHandler(ReqRespMethod.ExecutionPayloadEnvelopesByRoot), ], [ protocols.ExecutionPayloadEnvelopesByRange(fork, this.config), this.getHandler(ReqRespMethod.ExecutionPayloadEnvelopesByRange), ] ); } return protocolsAtFork; } protected onIncomingRequestBody(request: RequestTypedContainer, peer: PeerId): void { const peerClient = this.peersData.getPeerKind(peer.toString()) ?? ClientKind.Unknown; // 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 synchronous call, preventing the stream from ending cleanly callInNextEventLoop(() => this.networkEventBus.emit(NetworkEvent.reqRespRequest, {request, peer, peerClient})); } protected onIncomingRequest(peerId: PeerId, protocol: ProtocolDescriptor): void { // Remember preferred 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: ReqRespRequest, peerId: PeerId): AsyncIterable<ResponseOutgoing> { // Fork is ignored in responseSszTypeByMethod, type is determined by protocol version that is negotiated const type = responseSszTypeByMethod[ReqRespMethod.Status](ForkName.phase0, req.version); // Request uses the same type as response const body = type.deserialize(req.data); this.onIncomingRequestBody({method: ReqRespMethod.Status, body}, peerId); const status = this.statusCache.get(); yield { data: type.serialize(status), // Status topic is fork-agnostic boundary: {fork: ForkName.phase0, epoch: GENESIS_EPOCH}, }; } private async *onGoodbye(req: ReqRespRequest, peerId: PeerId): AsyncIterable<ResponseOutgoing> { const body = ssz.phase0.Goodbye.deserialize(req.data); this.onIncomingRequestBody({method: ReqRespMethod.Goodbye, body}, peerId); yield { data: ssz.phase0.Goodbye.serialize(BigInt(0)), // Goodbye topic is fork-agnostic boundary: {fork: ForkName.phase0, epoch: GENESIS_EPOCH}, }; } private async *onPing(req: ReqRespRequest, peerId: PeerId): AsyncIterable<ResponseOutgoing> { const body = ssz.phase0.Ping.deserialize(req.data); this.onIncomingRequestBody({method: ReqRespMethod.Ping, body}, peerId); yield { data: ssz.phase0.Ping.serialize(this.metadataController.seqNumber), // Ping topic is fork-agnostic boundary: {fork: ForkName.phase0, epoch: GENESIS_EPOCH}, }; } private async *onMetadata(req: ReqRespRequest, peerId: PeerId): AsyncIterable<ResponseOutgoing> { this.onIncomingRequestBody({method: ReqRespMethod.Metadata, body: null}, peerId); const metadata = this.metadataController.json; // Fork is ignored in responseSszTypeByMethod, type is determined by protocol version that is negotiated const type = responseSszTypeByMethod[ReqRespMethod.Metadata](ForkName.phase0, req.version); yield { data: type.serialize(metadata), // Metadata topic is fork-agnostic boundary: {fork: ForkName.phase0, epoch: GENESIS_EPOCH}, }; } }