UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

515 lines • 26.5 kB
import { peerIdFromPrivateKey } from "@libp2p/peer-id"; import { routes } from "@lodestar/api"; import { ForkSeq } from "@lodestar/params"; import { computeEpochAtSlot } from "@lodestar/state-transition"; import { isGloasDataColumnSidecar, } from "@lodestar/types"; import { prettyPrintIndices, sleep } from "@lodestar/utils"; import { ChainEvent } from "../chain/index.js"; import { computeSubnetForDataColumnSidecar } from "../chain/validation/dataColumnSidecar.js"; import { RegistryMetricCreator } from "../metrics/index.js"; import { peerIdToString } from "../util/peerId.js"; import { promiseAllMaybeAsync } from "../util/promises.js"; import { NetworkCore, WorkerNetworkCore } from "./core/index.js"; import { NetworkEvent, NetworkEventBus } from "./events.js"; import { getActiveForkBoundaries } from "./forks.js"; import { GossipType } from "./gossip/index.js"; import { getGossipSSZType, gossipTopicIgnoreDuplicatePublishError, stringifyGossipTopic } from "./gossip/topic.js"; import { AggregatorTracker } from "./processor/aggregatorTracker.js"; import { NetworkProcessor } from "./processor/index.js"; import { ReqRespMethod } from "./reqresp/index.js"; import { Version, requestSszTypeByMethod, responseSszTypeByMethod } from "./reqresp/types.js"; import { collectExactOneTyped, collectMaxResponseTyped, collectMaxResponseTypedWithBytes, } from "./reqresp/utils/collect.js"; import { collectSequentialBlocksInRange } from "./reqresp/utils/collectSequentialBlocksInRange.js"; import { isPublishToZeroPeersError, prettyPrintPeerIdStr } from "./util.js"; /** * Must support running both on worker and on main thread. * * Exists a front class that's what consumers interact with. * This class will multiplex between: * - libp2p in worker * - libp2p in main thread */ export class Network { peerId; custodyConfig; // TODO: Make private events; logger; config; clock; chain; // Used only for sleep() statements controller; // TODO: Review networkProcessor; core; aggregatorTracker; subscribedToCoreTopics = false; connectedPeersSyncMeta = new Map(); constructor(modules) { this.peerId = peerIdFromPrivateKey(modules.privateKey); this.config = modules.config; this.custodyConfig = modules.chain.custodyConfig; this.logger = modules.logger; this.chain = modules.chain; this.clock = modules.chain.clock; this.controller = new AbortController(); this.events = modules.networkEventBus; this.networkProcessor = modules.networkProcessor; this.core = modules.core; this.aggregatorTracker = modules.aggregatorTracker; this.events.on(NetworkEvent.peerConnected, this.onPeerConnected); this.events.on(NetworkEvent.peerDisconnected, this.onPeerDisconnected); this.chain.emitter.on(routes.events.EventType.head, this.onHead); this.chain.emitter.on(routes.events.EventType.lightClientFinalityUpdate, ({ data }) => this.onLightClientFinalityUpdate(data)); this.chain.emitter.on(routes.events.EventType.lightClientOptimisticUpdate, ({ data }) => this.onLightClientOptimisticUpdate(data)); this.chain.emitter.on(ChainEvent.updateTargetCustodyGroupCount, this.onTargetGroupCountUpdated); this.chain.emitter.on(ChainEvent.publishDataColumns, this.onPublishDataColumns); this.chain.emitter.on(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars); this.chain.emitter.on(ChainEvent.updateStatus, this.onUpdateStatus); } static async init({ opts, config, logger, metrics, chain, db, gossipHandlers, privateKey, peerStoreDir, getReqRespHandler, }) { const events = new NetworkEventBus(); const aggregatorTracker = new AggregatorTracker(); const activeValidatorCount = chain.getHeadState().activeValidatorCount; const initialStatus = chain.getStatus(); const initialCustodyGroupCount = chain.custodyConfig.targetCustodyGroupCount; if (opts.useWorker) { logger.info("running libp2p instance in worker thread"); } const core = opts.useWorker ? await WorkerNetworkCore.init({ opts: { ...opts, peerStoreDir, metricsEnabled: Boolean(metrics), activeValidatorCount, genesisTime: chain.genesisTime, initialStatus, initialCustodyGroupCount, }, config, privateKey, logger, events, metrics, getReqRespHandler, }) : await NetworkCore.init({ opts, config, privateKey, peerStoreDir, logger, clock: chain.clock, events, getReqRespHandler, metricsRegistry: metrics ? new RegistryMetricCreator() : null, initialStatus, initialCustodyGroupCount, activeValidatorCount, }); const networkProcessor = new NetworkProcessor({ chain, db, config, logger, metrics, events, gossipHandlers, core, aggregatorTracker }, opts); const multiaddresses = opts.localMultiaddrs?.join(","); const peerId = peerIdFromPrivateKey(privateKey); logger.info(`PeerId ${peerIdToString(peerId)}, Multiaddrs ${multiaddresses}`); return new Network({ opts, privateKey, config, logger, chain, networkEventBus: events, aggregatorTracker, networkProcessor, core, }); } get closed() { return this.controller.signal.aborted; } /** Destroy this instance. Can only be called once. */ async close() { if (this.closed) return; this.events.off(NetworkEvent.peerConnected, this.onPeerConnected); this.events.off(NetworkEvent.peerDisconnected, this.onPeerDisconnected); this.chain.emitter.off(routes.events.EventType.head, this.onHead); this.chain.emitter.off(routes.events.EventType.lightClientFinalityUpdate, this.onLightClientFinalityUpdate); this.chain.emitter.off(routes.events.EventType.lightClientOptimisticUpdate, this.onLightClientOptimisticUpdate); this.chain.emitter.off(ChainEvent.updateTargetCustodyGroupCount, this.onTargetGroupCountUpdated); this.chain.emitter.off(ChainEvent.publishDataColumns, this.onPublishDataColumns); this.chain.emitter.off(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars); this.chain.emitter.off(ChainEvent.updateStatus, this.onUpdateStatus); await this.core.close(); // Used only for sleep() statements this.controller.abort(); this.logger.debug("network core closed"); } async scrapeMetrics() { return this.core.scrapeMetrics(); } /** * Request att subnets up `toSlot`. Network will ensure to mantain some peers for each */ async prepareBeaconCommitteeSubnets(subscriptions) { for (const subscription of subscriptions) { if (subscription.isAggregator) { this.aggregatorTracker.addAggregator(subscription.subnet, subscription.slot); } } this.aggregatorTracker.prune(); return this.core.prepareBeaconCommitteeSubnets(subscriptions); } async prepareSyncCommitteeSubnets(subscriptions) { return this.core.prepareSyncCommitteeSubnets(subscriptions); } /** * The app layer needs to refresh the status of some peers. The sync have reached a target */ async reStatusPeers(peers) { return this.core.reStatusPeers(peers); } searchUnknownBlock(slotRoot, source, peer) { this.networkProcessor.searchUnknownBlock(slotRoot, source, peer); } searchUnknownEnvelope(slotRoot, source, peer) { this.networkProcessor.searchUnknownEnvelope(slotRoot, source, peer); } async reportPeer(peer, action, actionName) { return this.core.reportPeer(peer, action, actionName); } // REST API queries getConnectedPeers() { return Array.from(this.connectedPeersSyncMeta.keys()); } getConnectedPeerSyncMeta(peerId) { const syncMeta = this.connectedPeersSyncMeta.get(peerId); if (!syncMeta) { throw new Error(`peerId=${prettyPrintPeerIdStr(peerId)} not in connectedPeerSyncMeta`); } return { peerId, ...syncMeta }; } getConnectedPeerCount() { return this.connectedPeersSyncMeta.size; } async getNetworkIdentity() { return this.core.getNetworkIdentity(); } /** * Subscribe to all gossip events. Safe to call multiple times */ async subscribeGossipCoreTopics() { if (!this.subscribedToCoreTopics) { await this.core.subscribeGossipCoreTopics(); // Only mark subscribedToCoreTopics if worker resolved this call this.subscribedToCoreTopics = true; } } /** * Unsubscribe from all gossip events. Safe to call multiple times */ async unsubscribeGossipCoreTopics() { // Drop all the gossip validation queues this.networkProcessor.dropAllJobs(); await this.core.unsubscribeGossipCoreTopics(); this.subscribedToCoreTopics = false; } isSubscribedToGossipCoreTopics() { return this.subscribedToCoreTopics; } shouldAggregate(subnet, slot) { return this.aggregatorTracker.shouldAggregate(subnet, slot); } // Gossip async publishBeaconBlock(signedBlock) { const epoch = computeEpochAtSlot(signedBlock.message.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.beacon_block, boundary }, signedBlock, { ignoreDuplicatePublishError: true, }); } async publishBlobSidecar(blobSidecar) { const epoch = computeEpochAtSlot(blobSidecar.signedBlockHeader.message.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); const subnet = blobSidecar.index; return this.publishGossip({ type: GossipType.blob_sidecar, boundary, subnet }, blobSidecar, { ignoreDuplicatePublishError: true, }); } async publishDataColumnSidecar(dataColumnSidecar) { const slot = isGloasDataColumnSidecar(dataColumnSidecar) ? dataColumnSidecar.slot : dataColumnSidecar.signedBlockHeader.message.slot; const epoch = computeEpochAtSlot(slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); const subnet = computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar); return this.publishGossip({ type: GossipType.data_column_sidecar, boundary, subnet }, dataColumnSidecar, { ignoreDuplicatePublishError: true, // we ensure having all topic peers via prioritizePeers() function // in the worse case, if there is 0 peer on the topic, the overall publish operation could be still a success // because supernode will rebuild and publish missing data column sidecars for us // hence we want to track sent peers as 0 instead of an error allowPublishToZeroTopicPeers: true, }); } async publishBeaconAggregateAndProof(aggregateAndProof) { const epoch = computeEpochAtSlot(aggregateAndProof.message.aggregate.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.beacon_aggregate_and_proof, boundary }, aggregateAndProof, { ignoreDuplicatePublishError: true }); } async publishBeaconAttestation(attestation, subnet) { const epoch = computeEpochAtSlot(attestation.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.beacon_attestation, boundary, subnet }, attestation, { ignoreDuplicatePublishError: true }); } async publishVoluntaryExit(voluntaryExit) { const epoch = voluntaryExit.message.epoch; const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.voluntary_exit, boundary }, voluntaryExit, { ignoreDuplicatePublishError: true, }); } async publishBlsToExecutionChange(blsToExecutionChange) { const publishChanges = []; for (const boundary of getActiveForkBoundaries(this.config, this.clock.currentEpoch)) { const fork = ForkSeq[boundary.fork]; if (fork >= ForkSeq.capella) { const publishPromise = this.publishGossip({ type: GossipType.bls_to_execution_change, boundary }, blsToExecutionChange, { ignoreDuplicatePublishError: true }); publishChanges.push(publishPromise); } } if (publishChanges.length === 0) { throw Error("No capella+ fork active yet to publish blsToExecutionChange"); } return Promise.any(publishChanges); } async publishProposerSlashing(proposerSlashing) { const epoch = computeEpochAtSlot(Number(proposerSlashing.signedHeader1.message.slot)); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.proposer_slashing, boundary }, proposerSlashing); } async publishAttesterSlashing(attesterSlashing) { const epoch = computeEpochAtSlot(Number(attesterSlashing.attestation1.data.slot)); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.attester_slashing, boundary }, attesterSlashing); } async publishSyncCommitteeSignature(signature, subnet) { const epoch = computeEpochAtSlot(signature.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.sync_committee, boundary, subnet }, signature, { ignoreDuplicatePublishError: true, }); } async publishContributionAndProof(contributionAndProof) { const epoch = computeEpochAtSlot(contributionAndProof.message.contribution.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.sync_committee_contribution_and_proof, boundary }, contributionAndProof, { ignoreDuplicatePublishError: true }); } async publishLightClientFinalityUpdate(update) { const epoch = computeEpochAtSlot(update.signatureSlot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.light_client_finality_update, boundary }, update); } async publishLightClientOptimisticUpdate(update) { const epoch = computeEpochAtSlot(update.signatureSlot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.light_client_optimistic_update, boundary }, update); } async publishSignedExecutionPayloadEnvelope(signedEnvelope) { const epoch = computeEpochAtSlot(signedEnvelope.message.payload.slotNumber); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.execution_payload, boundary }, signedEnvelope, { ignoreDuplicatePublishError: true }); } async publishPayloadAttestationMessage(payloadAttestationMessage) { const epoch = computeEpochAtSlot(payloadAttestationMessage.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.payload_attestation_message, boundary }, payloadAttestationMessage, { ignoreDuplicatePublishError: true }); } async publishGossip(topic, object, opts) { const topicStr = stringifyGossipTopic(this.config, topic); const sszType = getGossipSSZType(topic); const messageData = sszType.serialize(object); opts = { ...opts, ignoreDuplicatePublishError: gossipTopicIgnoreDuplicatePublishError[topic.type], }; const sentPeers = await this.core.publishGossip(topicStr, messageData, opts); this.logger.verbose("Publish to topic", { topic: topicStr, sentPeers, currentSlot: this.clock.currentSlot }); return sentPeers; } // ReqResp async sendBeaconBlocksByRange(peerId, request) { return collectSequentialBlocksInRange(this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByRange, // Before altair, prioritize V2. After altair only request V2 this.config.getForkSeq(this.clock.currentSlot) >= ForkSeq.altair ? [Version.V2] : [Version.V2, Version.V1], request), request, this.chain.serializedCache); } async sendBeaconBlocksByRoot(peerId, request) { return collectMaxResponseTypedWithBytes(this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByRoot, // Before altair, prioritize V2. After altair only request V2 this.config.getForkSeq(this.clock.currentSlot) >= ForkSeq.altair ? [Version.V2] : [Version.V2, Version.V1], request), request.length, responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByRoot], this.chain.serializedCache); } async sendBeaconBlocksByHead(peerId, request) { return collectMaxResponseTypedWithBytes(this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByHead, [Version.V1], request), Math.min(request.count, this.config.MAX_REQUEST_BLOCKS_DENEB), responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByHead], this.chain.serializedCache); } async sendLightClientBootstrap(peerId, request) { return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientBootstrap, [Version.V1], request), responseSszTypeByMethod[ReqRespMethod.LightClientBootstrap]); } async sendLightClientOptimisticUpdate(peerId) { return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientOptimisticUpdate, [Version.V1], null), responseSszTypeByMethod[ReqRespMethod.LightClientOptimisticUpdate]); } async sendLightClientFinalityUpdate(peerId) { return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientFinalityUpdate, [Version.V1], null), responseSszTypeByMethod[ReqRespMethod.LightClientFinalityUpdate]); } async sendLightClientUpdatesByRange(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientUpdatesByRange, [Version.V1], request), request.count, responseSszTypeByMethod[ReqRespMethod.LightClientUpdatesByRange]); } async sendBlobSidecarsByRange(peerId, request) { const epoch = computeEpochAtSlot(request.startSlot); return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.BlobSidecarsByRange, [Version.V1], request), // request's count represent the slots, so the actual max count received could be slots * blobs per slot request.count * this.config.getMaxBlobsPerBlock(epoch), responseSszTypeByMethod[ReqRespMethod.BlobSidecarsByRange]); } async sendBlobSidecarsByRoot(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.BlobSidecarsByRoot, [Version.V1], request), request.length, responseSszTypeByMethod[ReqRespMethod.BlobSidecarsByRoot], this.chain.serializedCache); } async sendDataColumnSidecarsByRange(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.DataColumnSidecarsByRange, [Version.V1], request), request.count * request.columns.length, responseSszTypeByMethod[ReqRespMethod.DataColumnSidecarsByRange]); } async sendDataColumnSidecarsByRoot(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.DataColumnSidecarsByRoot, [Version.V1], request), request.reduce((total, { columns }) => total + columns.length, 0), responseSszTypeByMethod[ReqRespMethod.DataColumnSidecarsByRoot], this.chain.serializedCache); } async sendExecutionPayloadEnvelopesByRange(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.ExecutionPayloadEnvelopesByRange, [Version.V1], request), request.count, responseSszTypeByMethod[ReqRespMethod.ExecutionPayloadEnvelopesByRange]); } async sendExecutionPayloadEnvelopesByRoot(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.ExecutionPayloadEnvelopesByRoot, [Version.V1], request), request.length, responseSszTypeByMethod[ReqRespMethod.ExecutionPayloadEnvelopesByRoot], this.chain.serializedCache); } sendReqRespRequest(peerId, method, versions, request) { const fork = this.config.getForkName(this.clock.currentSlot); const requestType = requestSszTypeByMethod(fork, this.config)[method]; const requestData = requestType ? requestType.serialize(request) : new Uint8Array(); // ReqResp outgoing request, emit from main thread to worker return this.core.sendReqRespRequest({ peerId, method, versions, requestData }); } // Debug connectToPeer(peer, multiaddr) { return this.core.connectToPeer(peer, multiaddr); } disconnectPeer(peer) { return this.core.disconnectPeer(peer); } addDirectPeer(peer) { return this.core.addDirectPeer(peer); } removeDirectPeer(peerId) { return this.core.removeDirectPeer(peerId); } getDirectPeers() { return this.core.getDirectPeers(); } dumpPeer(peerIdStr) { return this.core.dumpPeer(peerIdStr); } dumpPeers() { return this.core.dumpPeers(); } dumpPeerScoreStats() { return this.core.dumpPeerScoreStats(); } dumpGossipPeerScoreStats() { return this.core.dumpGossipPeerScoreStats(); } dumpDiscv5KadValues() { return this.core.dumpDiscv5KadValues(); } dumpMeshPeers() { return this.core.dumpMeshPeers(); } async dumpGossipQueue(gossipType) { return this.networkProcessor.dumpGossipQueue(gossipType); } async writeNetworkThreadProfile(durationMs, dirpath) { return this.core.writeNetworkThreadProfile(durationMs, dirpath); } async writeDiscv5Profile(durationMs, dirpath) { return this.core.writeDiscv5Profile(durationMs, dirpath); } async writeNetworkHeapSnapshot(prefix, dirpath) { return this.core.writeNetworkHeapSnapshot(prefix, dirpath); } async writeDiscv5HeapSnapshot(prefix, dirpath) { return this.core.writeDiscv5HeapSnapshot(prefix, dirpath); } onLightClientFinalityUpdate = async (finalityUpdate) => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) try { // messages SHOULD be broadcast after SYNC_MESSAGE_DUE_BPS of slot has transpired // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/altair/light-client/p2p-interface.md#sync-committee await this.waitForSyncMessageCutoff(finalityUpdate.signatureSlot); await this.publishLightClientFinalityUpdate(finalityUpdate); } catch (e) { // Non-mandatory route on most of network as of Oct 2022. May not have found any peers on topic yet // Remove once https://github.com/ChainSafe/js-libp2p-gossipsub/issues/367 if (!isPublishToZeroPeersError(e)) { this.logger.debug("Error on BeaconGossipHandler.onLightclientFinalityUpdate", {}, e); } } }; onLightClientOptimisticUpdate = async (optimisticUpdate) => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) try { // messages SHOULD be broadcast after SYNC_MESSAGE_DUE_BPS of slot has transpired // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/altair/light-client/p2p-interface.md#sync-committee await this.waitForSyncMessageCutoff(optimisticUpdate.signatureSlot); await this.publishLightClientOptimisticUpdate(optimisticUpdate); } catch (e) { // Non-mandatory route on most of network as of Oct 2022. May not have found any peers on topic yet // Remove once https://github.com/ChainSafe/js-libp2p-gossipsub/issues/367 if (!isPublishToZeroPeersError(e)) { this.logger.debug("Error on BeaconGossipHandler.onLightclientOptimisticUpdate", {}, e); } } }; waitForSyncMessageCutoff = async (slot) => { const fork = this.config.getForkName(slot); const msToCutoffTime = this.config.getSyncMessageDueMs(fork) - this.chain.clock.msFromSlot(slot); await sleep(msToCutoffTime, this.controller.signal); }; onHead = async () => { await this.onUpdateStatus(); }; onPeerConnected = (data) => { const { peer, clientAgent, custodyColumns, status } = data; const earliestAvailableSlot = status.earliestAvailableSlot; this.logger.verbose("onPeerConnected", { peer, clientAgent, custodyColumns: prettyPrintIndices(custodyColumns), earliestAvailableSlot: earliestAvailableSlot ?? "pre-fulu", }); this.connectedPeersSyncMeta.set(peer, { client: clientAgent, custodyColumns, earliestAvailableSlot, // can be undefined pre-fulu }); }; onPeerDisconnected = (data) => { this.connectedPeersSyncMeta.delete(data.peer); }; onTargetGroupCountUpdated = (count) => { this.core.setTargetGroupCount(count); }; onPublishDataColumns = (sidecars) => { return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar))); }; onPublishBlobSidecars = (sidecars) => { return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishBlobSidecar(sidecar))); }; onUpdateStatus = async () => { await this.core.updateStatus(this.chain.getStatus()); }; } //# sourceMappingURL=network.js.map