@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
207 lines • 11.4 kB
JavaScript
import { ForkName, ForkSeq, GENESIS_EPOCH } from "@lodestar/params";
import { Encoding, ReqResp, } from "@lodestar/reqresp";
import { ssz } from "@lodestar/types";
import { callInNextEventLoop } from "../../util/eventLoop.js";
import { NetworkEvent } from "../events.js";
import { PeerAction } from "../peers/score/index.js";
import * as protocols from "./protocols.js";
import { onOutgoingReqRespError } from "./score.js";
import { ReqRespMethod, Version, requestSszTypeByMethod, responseSszTypeByMethod, } from "./types.js";
import { collectExactOneTyped } from "./utils/collect.js";
export { getReqRespHandlers } from "./handlers/index.js";
export { ReqRespMethod } from "./types.js";
/**
* 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 {
constructor(modules, options = {}) {
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);
},
});
/** Track registered fork to only send to known protocols */
this.currentRegisteredFork = ForkSeq.phase0;
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() {
await super.start();
}
async stop() {
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) {
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, method, versions, requestData) {
// 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) {
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, request) {
return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.Status, [Version.V1], request), responseSszTypeByMethod[ReqRespMethod.Status]);
}
async sendGoodbye(peerId, request) {
// TODO: Replace with "ignore response after request"
await collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.Goodbye, [Version.V1], request), responseSszTypeByMethod[ReqRespMethod.Goodbye]);
}
async sendMetadata(peerId) {
return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.Metadata,
// Before altair, prioritize V2. After altair only request V2
this.currentRegisteredFork >= ForkSeq.altair ? [Version.V2] : [(Version.V2, Version.V1)], null), responseSszTypeByMethod[ReqRespMethod.Metadata]);
}
sendReqRespRequest(peerId, method, versions, request) {
const fork = ForkName[ForkSeq[this.currentRegisteredFork]];
const requestType = requestSszTypeByMethod(fork, this.config)[method];
const requestData = requestType ? requestType.serialize(request) : 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.
*/
getProtocolsAtBoundary(boundary) {
const { fork } = boundary;
const protocolsAtFork = [
[protocols.Ping(fork, this.config), this.onPing.bind(this)],
[protocols.Status(fork, this.config), this.onStatus.bind(this)],
[protocols.Goodbye(fork, this.config), this.onGoodbye.bind(this)],
// Support V2 methods as soon as implemented (for altair)
// Ref https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/p2p-interface.md#transitioning-from-v1-to-v2
[protocols.MetadataV2(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)]);
}
return protocolsAtFork;
}
onIncomingRequestBody(request, peer) {
// 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 }));
}
onIncomingRequest(peerId, protocol) {
// Remember preferred encoding
if (protocol.method === ReqRespMethod.Status) {
this.peersData.setEncodingPreference(peerId.toString(), protocol.encoding);
}
}
onOutgoingRequestError(peerId, method, error) {
const peerAction = onOutgoingReqRespError(error, method);
if (peerAction !== null) {
this.peerRpcScores.applyAction(peerId, peerAction, error.type.code);
}
}
async *onStatus(req, peerId) {
const body = ssz.phase0.Status.deserialize(req.data);
this.onIncomingRequestBody({ method: ReqRespMethod.Status, body }, peerId);
yield {
data: ssz.phase0.Status.serialize(this.statusCache.get()),
// Status topic is fork-agnostic
boundary: { fork: ForkName.phase0, epoch: GENESIS_EPOCH },
};
}
async *onGoodbye(req, peerId) {
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 },
};
}
async *onPing(req, peerId) {
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 },
};
}
async *onMetadata(req, peerId) {
this.onIncomingRequestBody({ method: ReqRespMethod.Metadata, body: null }, peerId);
const metadata = this.metadataController.json;
// Metadata topic is fork-agnostic
const fork = ForkName.phase0;
const epoch = GENESIS_EPOCH;
const type = responseSszTypeByMethod[ReqRespMethod.Metadata](fork, req.version);
yield {
data: type.serialize(metadata),
boundary: { fork, epoch },
};
}
}
//# sourceMappingURL=ReqRespBeaconNode.js.map