diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 254fdae18685..6dbe216d069a 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -14,8 +14,8 @@ import {ChainEvent, IBeaconChain, IBeaconClock} from "../chain/index.js"; import {BlockInput, BlockInputType, getBlockInput} from "../chain/blocks/types.js"; import {INetworkOptions} from "./options.js"; import {INetwork} from "./interface.js"; -import {IReqRespBeaconNode, ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/ReqRespBeaconNode.js"; -import {Eth2Gossipsub, getGossipHandlers, GossipHandlers, GossipType} from "./gossip/index.js"; +import {ReqRespBeaconNode, ReqRespHandlers} from "./reqresp/ReqRespBeaconNode.js"; +import {Eth2Gossipsub, getGossipHandlers, GossipHandlers, GossipTopicTypeMap, GossipType} from "./gossip/index.js"; import {MetadataController} from "./metadata.js"; import {FORK_EPOCH_LOOKAHEAD, getActiveForks} from "./forks.js"; import {PeerManager} from "./peers/peerManager.js"; @@ -39,7 +39,7 @@ interface INetworkModules { export class Network implements INetwork { events: INetworkEventBus; - reqResp: IReqRespBeaconNode; + reqResp: ReqRespBeaconNode; attnetsService: AttnetsService; syncnetsService: SyncnetsService; gossip: Eth2Gossipsub; @@ -144,8 +144,16 @@ export class Network implements INetwork { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call (this.libp2p.connectionManager as DefaultConnectionManager)["latencyMonitor"].stop(); - this.reqResp.start(); - this.metadata.start(this.getEnr(), this.config.getForkName(this.clock.currentSlot)); + // Network spec decides version changes based on clock fork, not head fork + const forkCurrentSlot = this.config.getForkName(this.clock.currentSlot); + + // Register only ReqResp protocols relevant to clock's fork + await this.reqResp.start(); + this.reqResp.registerProtocolsAtFork(forkCurrentSlot); + + // Initialize ENR with clock's fork + this.metadata.start(this.getEnr(), forkCurrentSlot); + await this.peerManager.start(); await this.gossip.start(); this.attnetsService.start(); @@ -162,7 +170,10 @@ export class Network implements INetwork { await this.peerManager.goodbyeAndDisconnectAllPeers(); await this.peerManager.stop(); await this.gossip.stop(); - this.reqResp.stop(); + + await this.reqResp.stop(); + await this.reqResp.unregisterAllProtocols(); + this.attnetsService.stop(); this.syncnetsService.stop(); await this.libp2p.stop(); @@ -349,9 +360,13 @@ export class Network implements INetwork { // Before fork transition if (epoch === forkEpoch - FORK_EPOCH_LOOKAHEAD) { - this.logger.info("Subscribing gossip topics to next fork", {nextFork}); // Don't subscribe to new fork if the node is not subscribed to any topic - if (this.isSubscribedToGossipCoreTopics()) this.subscribeCoreTopicsAtFork(nextFork); + if (this.isSubscribedToGossipCoreTopics()) { + this.subscribeCoreTopicsAtFork(nextFork); + this.logger.info("Subscribing gossip topics before fork", {nextFork}); + } else { + this.logger.info("Skipping subscribing gossip topics before fork", {nextFork}); + } this.attnetsService.subscribeSubnetsToNextFork(nextFork); this.syncnetsService.subscribeSubnetsToNextFork(nextFork); } @@ -360,6 +375,7 @@ export class Network implements INetwork { if (epoch === forkEpoch) { // updateEth2Field() MUST be called with clock epoch, onEpoch event is emitted in response to clock events this.metadata.updateEth2Field(epoch); + this.reqResp.registerProtocolsAtFork(nextFork); } // After fork transition @@ -380,25 +396,8 @@ export class Network implements INetwork { if (this.subscribedForks.has(fork)) return; this.subscribedForks.add(fork); - this.gossip.subscribeTopic({type: GossipType.beacon_block, fork}); - this.gossip.subscribeTopic({type: GossipType.beacon_aggregate_and_proof, fork}); - this.gossip.subscribeTopic({type: GossipType.voluntary_exit, fork}); - this.gossip.subscribeTopic({type: GossipType.proposer_slashing, fork}); - this.gossip.subscribeTopic({type: GossipType.attester_slashing, fork}); - // Any fork after altair included - if (fork !== ForkName.phase0) { - this.gossip.subscribeTopic({type: GossipType.sync_committee_contribution_and_proof, fork}); - this.gossip.subscribeTopic({type: GossipType.light_client_optimistic_update, fork}); - this.gossip.subscribeTopic({type: GossipType.light_client_finality_update, fork}); - } - - if (this.opts.subscribeAllSubnets) { - for (let subnet = 0; subnet < ATTESTATION_SUBNET_COUNT; subnet++) { - this.gossip.subscribeTopic({type: GossipType.beacon_attestation, fork, subnet}); - } - for (let subnet = 0; subnet < SYNC_COMMITTEE_SUBNET_COUNT; subnet++) { - this.gossip.subscribeTopic({type: GossipType.sync_committee, fork, subnet}); - } + for (const topic of this.coreTopicsAtFork(fork)) { + this.gossip.subscribeTopic({...topic, fork}); } }; @@ -406,27 +405,44 @@ export class Network implements INetwork { if (!this.subscribedForks.has(fork)) return; this.subscribedForks.delete(fork); - this.gossip.unsubscribeTopic({type: GossipType.beacon_block, fork}); - this.gossip.unsubscribeTopic({type: GossipType.beacon_aggregate_and_proof, fork}); - this.gossip.unsubscribeTopic({type: GossipType.voluntary_exit, fork}); - this.gossip.unsubscribeTopic({type: GossipType.proposer_slashing, fork}); - this.gossip.unsubscribeTopic({type: GossipType.attester_slashing, fork}); + for (const topic of this.coreTopicsAtFork(fork)) { + this.gossip.unsubscribeTopic({...topic, fork}); + } + }; + + /** + * De-duplicate logic to pick fork topics between subscribeCoreTopicsAtFork and unsubscribeCoreTopicsAtFork + */ + private coreTopicsAtFork(fork: ForkName): GossipTopicTypeMap[keyof GossipTopicTypeMap][] { + // Common topics for all forks + const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [ + {type: GossipType.beacon_block}, + {type: GossipType.beacon_aggregate_and_proof}, + {type: GossipType.voluntary_exit}, + {type: GossipType.proposer_slashing}, + {type: GossipType.attester_slashing}, + ]; + // Any fork after altair included - if (fork !== ForkName.phase0) { - this.gossip.unsubscribeTopic({type: GossipType.sync_committee_contribution_and_proof, fork}); - this.gossip.unsubscribeTopic({type: GossipType.light_client_optimistic_update, fork}); - this.gossip.unsubscribeTopic({type: GossipType.light_client_finality_update, fork}); + if (ForkSeq[fork] >= ForkSeq.altair) { + topics.push({type: GossipType.sync_committee_contribution_and_proof}); + topics.push({type: GossipType.light_client_optimistic_update}); + topics.push({type: GossipType.light_client_finality_update}); } if (this.opts.subscribeAllSubnets) { for (let subnet = 0; subnet < ATTESTATION_SUBNET_COUNT; subnet++) { - this.gossip.unsubscribeTopic({type: GossipType.beacon_attestation, fork, subnet}); + topics.push({type: GossipType.beacon_attestation, subnet}); } - for (let subnet = 0; subnet < SYNC_COMMITTEE_SUBNET_COUNT; subnet++) { - this.gossip.unsubscribeTopic({type: GossipType.sync_committee, fork, subnet}); + if (ForkSeq[fork] >= ForkSeq.altair) { + for (let subnet = 0; subnet < SYNC_COMMITTEE_SUBNET_COUNT; subnet++) { + topics.push({type: GossipType.sync_committee, subnet}); + } } } - }; + + return topics; + } private onLightClientFinalityUpdate = async (finalityUpdate: altair.LightClientFinalityUpdate): Promise => { if (this.hasAttachedSyncCommitteeMember()) { diff --git a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts index 21736f068926..7261aaad13ab 100644 --- a/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts +++ b/packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts @@ -1,7 +1,7 @@ import {PeerId} from "@libp2p/interface-peer-id"; import {Libp2p} from "libp2p"; import {IBeaconConfig} from "@lodestar/config"; -import {ForkName} from "@lodestar/params"; +import {ForkName, ForkSeq} from "@lodestar/params"; import { collectExactOne, collectMaxResponse, @@ -36,6 +36,9 @@ export {IReqRespBeaconNode}; /** This type helps response to beacon_block_by_range and beacon_block_by_root more efficiently */ export type ReqRespBlockResponse = EncodedPayload; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ProtocolDefinitionAny = ProtocolDefinition; + export interface ReqRespBeaconNodeModules { libp2p: Libp2p; peersData: PeersData; @@ -73,6 +76,12 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { private readonly networkEventBus: INetworkEventBus; private readonly peersData: PeersData; + /** Track registered fork to only send to known protocols */ + private currentRegisteredFork: ForkSeq = ForkSeq.phase0; + + private readonly config: IBeaconConfig; + protected readonly logger: ILogger; + constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) { const {reqRespHandlers, networkEventBus, peersData, peerRpcScores, metadata, logger, metrics} = modules; @@ -81,6 +90,8 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { this.reqRespHandlers = reqRespHandlers; this.peerRpcScores = peerRpcScores; this.peersData = peersData; + this.config = modules.config; + this.logger = modules.logger; this.metadataController = metadata; this.networkEventBus = networkEventBus; this.inboundRateLimiter = new InboundRateLimiter(options, { @@ -88,21 +99,6 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { 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 { @@ -119,6 +115,30 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { this.inboundRateLimiter.prune(peerId); } + registerProtocolsAtFork(fork: ForkName): void { + this.currentRegisteredFork = ForkSeq[fork]; + + const mustSubscribeProtocols = this.getProtocolsAtFork(fork); + 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, prevent libp2p for throwing if already registered + for (const protocol of mustSubscribeProtocols) { + this.registerProtocol(protocol, {ignoreIfDuplicate: true}).catch((e) => { + this.logger.error("Error on ReqResp.registerProtocol", {protocolID: this.formatProtocolID(protocol)}, e); + }); + } + } + async status(peerId: PeerId, request: phase0.Status): Promise { return collectExactOne( this.sendRequest(peerId, ReqRespMethod.Status, [Version.V1], request) @@ -143,10 +163,16 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { ); } - 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 metadata(peerId: PeerId): Promise { + return collectExactOne( + this.sendRequest( + peerId, + ReqRespMethod.Metadata, + // Before altair, prioritize V2. After altair only request V2 + this.currentRegisteredFork >= ForkSeq.altair ? [Version.V2] : [(Version.V2, Version.V1)], + null + ) + ); } async beaconBlocksByRange( @@ -157,7 +183,8 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { this.sendRequest( peerId, ReqRespMethod.BeaconBlocksByRange, - [Version.V2, Version.V1], // Prioritize V2 + // Before altair, prioritize V2. After altair only request V2 + this.currentRegisteredFork >= ForkSeq.altair ? [Version.V2] : [(Version.V2, Version.V1)], request ), request @@ -172,7 +199,8 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { this.sendRequest( peerId, ReqRespMethod.BeaconBlocksByRoot, - [Version.V2, Version.V1], // Prioritize V2 + // Before altair, prioritize V2. After altair only request V2 + this.currentRegisteredFork >= ForkSeq.altair ? [Version.V2] : [(Version.V2, Version.V1)], request ), request.length @@ -242,6 +270,46 @@ export class ReqRespBeaconNode extends ReqResp implements IReqRespBeaconNode { ); } + /** + * Returns the list of protocols that must be subscribed during a specific fork. + * Any protocol not in this list must be un-subscribed. + */ + private getProtocolsAtFork(fork: ForkName): ProtocolDefinitionAny[] { + const modules = {config: this.config}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const protocols: ProtocolDefinition[] = [ + messages.Ping(this.onPing.bind(this)), + messages.Status(modules, this.onStatus.bind(this)), + messages.Goodbye(modules, 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 + messages.MetadataV2(modules, this.onMetadata.bind(this)), + messages.BeaconBlocksByRangeV2(modules, this.onBeaconBlocksByRange.bind(this)), + messages.BeaconBlocksByRootV2(modules, this.onBeaconBlocksByRoot.bind(this)), + ]; + + if (ForkSeq[fork] < ForkSeq.altair) { + // Unregister V1 topics at the fork boundary, so only declare for pre-altair + protocols.push( + messages.Metadata(modules, this.onMetadata.bind(this)), + messages.BeaconBlocksByRange(modules, this.onBeaconBlocksByRange.bind(this)), + messages.BeaconBlocksByRoot(modules, this.onBeaconBlocksByRoot.bind(this)) + ); + } + + if (ForkSeq[fork] >= ForkSeq.altair) { + // Should be okay to enable before altair, but for consistency only enable afterwards + protocols.push( + messages.LightClientBootstrap(modules, this.reqRespHandlers.onLightClientBootstrap), + messages.LightClientFinalityUpdate(modules, this.reqRespHandlers.onLightClientFinalityUpdate), + messages.LightClientOptimisticUpdate(modules, this.reqRespHandlers.onLightClientOptimisticUpdate), + messages.LightClientUpdatesByRange(modules, this.reqRespHandlers.onLightClientUpdatesByRange) + ); + } + + return protocols; + } + protected sendRequest(peerId: PeerId, method: string, versions: number[], body: Req): AsyncIterable { // Remember prefered encoding const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; diff --git a/packages/beacon-node/src/network/reqresp/interface.ts b/packages/beacon-node/src/network/reqresp/interface.ts index f4427c2e8d08..4039e86ab79f 100644 --- a/packages/beacon-node/src/network/reqresp/interface.ts +++ b/packages/beacon-node/src/network/reqresp/interface.ts @@ -1,5 +1,4 @@ import {PeerId} from "@libp2p/interface-peer-id"; -import {ForkName} from "@lodestar/params"; import {allForks, altair, eip4844, phase0} from "@lodestar/types"; export interface IReqRespBeaconNode { @@ -8,7 +7,7 @@ export interface IReqRespBeaconNode { status(peerId: PeerId, request: phase0.Status): Promise; goodbye(peerId: PeerId, request: phase0.Goodbye): Promise; ping(peerId: PeerId): Promise; - metadata(peerId: PeerId, fork?: ForkName): Promise; + metadata(peerId: PeerId): Promise; beaconBlocksByRange( peerId: PeerId, request: phase0.BeaconBlocksByRangeRequest diff --git a/packages/beacon-node/test/e2e/network/reqresp.test.ts b/packages/beacon-node/test/e2e/network/reqresp.test.ts index 8a8465d16cfb..de9ddd92c7c2 100644 --- a/packages/beacon-node/test/e2e/network/reqresp.test.ts +++ b/packages/beacon-node/test/e2e/network/reqresp.test.ts @@ -2,9 +2,8 @@ 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 {ForkName} from "@lodestar/params"; +import {createIBeaconConfig, createIChainForkConfig, IChainForkConfig} from "@lodestar/config"; +import {chainConfig} from "@lodestar/config/default"; import { Encoding, RequestError, @@ -13,27 +12,23 @@ import { HandlerTypeFromMessage, EncodedPayloadType, EncodedPayload, + ContextBytesType, } from "@lodestar/reqresp"; import * as messages from "@lodestar/reqresp/messages"; -import {altair, phase0, Root, ssz} from "@lodestar/types"; +import {allForks, 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 {ReqRespHandlers} from "../../../src/network/reqresp/handlers/index.js"; import {ReqRespMethod} from "../../../src/network/reqresp/types.js"; -import { - blocksToReqRespBlockResponses, - generateEmptyReqRespBlockResponse, - generateEmptySignedBlock, -} from "../../utils/block.js"; import {expectRejectedWithLodestarError} from "../../utils/errors.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 {arrToSource, generateEmptySignedBlocks} from "../../unit/network/reqresp/utils.js"; +import {arrToSource} from "../../unit/network/reqresp/utils.js"; import {defaultRateLimiterOpts} from "../../../src/network/reqresp/inboundRateLimiter.js"; /* eslint-disable require-yield, @typescript-eslint/naming-convention */ @@ -53,7 +48,14 @@ describe("network / ReqResp", function () { discv5FirstQueryDelayMs: 0, discv5: null, }; - const state = generateState(); + + // Schedule ALTAIR_FORK_EPOCH to trigger registering lightclient ReqResp protocols immediately + const config = createIChainForkConfig({ + ...chainConfig, + ALTAIR_FORK_EPOCH: 0, + }); + + const state = generateState({}, config); const beaconConfig = createIBeaconConfig(config, state.genesisValidatorsRoot); const chain = new MockBeaconChain({genesisTime: 0, chainId: 0, networkId: BigInt(0), state, config: beaconConfig}); const db = new StubbedBeaconDb(); @@ -143,18 +145,6 @@ describe("network / ReqResp", function () { expect(pong.toString()).to.deep.equal(expectedPong.toString(), "Wrong response body"); }); - it("should send/receive a metadata message - phase0", async function () { - const [netA, netB] = await createAndConnectPeers(); - - const metadata: phase0.Metadata = { - seqNumber: netB.metadata.seqNumber, - attnets: netB.metadata.attnets, - }; - - const receivedMetadata = await netA.reqResp.metadata(netB.peerId, ForkName.phase0); - expect(receivedMetadata).to.deep.equal(metadata, "Wrong response body"); - }); - it("should send/receive a metadata message - altair", async function () { const [netA, netB] = await createAndConnectPeers(); @@ -193,14 +183,16 @@ describe("network / ReqResp", function () { const req: phase0.BeaconBlocksByRangeRequest = {startSlot: 0, step: 1, count: 2}; const blocks: phase0.SignedBeaconBlock[] = []; for (let slot = req.startSlot; slot < req.count; slot++) { - const block = generateEmptySignedBlock(); + const block = config.getForkTypes(slot).SignedBeaconBlock.defaultValue(); block.message.slot = slot; blocks.push(block); } const [netA, netB] = await createAndConnectPeers({ onBeaconBlocksByRange: async function* () { - yield* arrToSource(blocksToReqRespBlockResponses(blocks)); + for (const block of blocks) { + yield wrapBlockAsEncodedPayload(config, block); + } } as HandlerTypeFromMessage, }); @@ -321,7 +313,11 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers({ onBeaconBlocksByRange: async function* onRequest() { - yield* arrToSource(blocksToReqRespBlockResponses(generateEmptySignedBlocks(2))); + for (let slot = 0; slot < 2; slot++) { + const block = config.getForkTypes(slot).SignedBeaconBlock.defaultValue(); + block.message.slot = slot; + yield wrapBlockAsEncodedPayload(config, block); + } throw Error(testErrorMessage); } as HandlerTypeFromMessage, }); @@ -343,7 +339,7 @@ describe("network / ReqResp", function () { onBeaconBlocksByRange: async function* onRequest() { // Wait for too long before sending first response chunk await sleep(ttfbTimeoutMs * 10); - yield generateEmptyReqRespBlockResponse(); + yield config.getForkTypes(0).SignedBeaconBlock.defaultValue(); } as HandlerTypeFromMessage, }, {ttfbTimeoutMs} @@ -364,10 +360,10 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers( { onBeaconBlocksByRange: async function* onRequest() { - yield generateEmptyReqRespBlockResponse(); + yield getEmptyEncodedPayloadSignedBeaconBlock(config); // Wait for too long before sending second response chunk await sleep(respTimeoutMs * 5); - yield generateEmptyReqRespBlockResponse(); + yield getEmptyEncodedPayloadSignedBeaconBlock(config); } as HandlerTypeFromMessage, }, {respTimeoutMs} @@ -405,7 +401,7 @@ describe("network / ReqResp", function () { const [netA, netB] = await createAndConnectPeers( { onBeaconBlocksByRange: async function* onRequest() { - yield generateEmptyReqRespBlockResponse(); + yield getEmptyEncodedPayloadSignedBeaconBlock(config); await sleep(100000000); } as HandlerTypeFromMessage, }, @@ -426,3 +422,21 @@ describe("network / ReqResp", function () { function formatMetadata(method: ReqRespMethod, encoding: Encoding, peer: PeerId): IRequestErrorMetadata { return {method, encoding, peer: peer.toString()}; } + +function getEmptyEncodedPayloadSignedBeaconBlock(config: IChainForkConfig): EncodedPayload { + return wrapBlockAsEncodedPayload(config, config.getForkTypes(0).SignedBeaconBlock.defaultValue()); +} + +function wrapBlockAsEncodedPayload( + config: IChainForkConfig, + block: allForks.SignedBeaconBlock +): EncodedPayload { + return { + type: EncodedPayloadType.bytes, + bytes: config.getForkTypes(block.message.slot).SignedBeaconBlock.serialize(block), + contextBytes: { + type: ContextBytesType.ForkDigest, + forkSlot: block.message.slot, + }, + }; +} diff --git a/packages/beacon-node/test/spec/presets/epoch_processing.ts b/packages/beacon-node/test/spec/presets/epoch_processing.ts index 76e9ac871f06..73a465af5fd9 100644 --- a/packages/beacon-node/test/spec/presets/epoch_processing.ts +++ b/packages/beacon-node/test/spec/presets/epoch_processing.ts @@ -8,7 +8,7 @@ import * as epochFns from "@lodestar/state-transition/epoch"; import {ssz} from "@lodestar/types"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; import {TestRunnerFn} from "../utils/types.js"; import {assertCorrectProgressiveBalances} from "../config.js"; diff --git a/packages/beacon-node/test/spec/presets/finality.ts b/packages/beacon-node/test/spec/presets/finality.ts index b94b10e86456..083aa1d496d5 100644 --- a/packages/beacon-node/test/spec/presets/finality.ts +++ b/packages/beacon-node/test/spec/presets/finality.ts @@ -9,7 +9,7 @@ import {ForkName} from "@lodestar/params"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; import {shouldVerify, TestRunnerFn} from "../utils/types.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; import {assertCorrectProgressiveBalances} from "../config.js"; /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/packages/beacon-node/test/spec/presets/fork_choice.ts b/packages/beacon-node/test/spec/presets/fork_choice.ts index acc8ebe8b4c2..ce8f3d5d174a 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.ts @@ -9,7 +9,7 @@ import {createIBeaconConfig} from "@lodestar/config"; import {BeaconChain, ChainEvent} from "../../../src/chain/index.js"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {testLogger} from "../../utils/logger.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; import {TestRunnerFn} from "../utils/types.js"; import {Eth1ForBlockProductionDisabled} from "../../../src/eth1/index.js"; import {ExecutionEngineMock} from "../../../src/execution/index.js"; diff --git a/packages/beacon-node/test/spec/presets/genesis.ts b/packages/beacon-node/test/spec/presets/genesis.ts index 75c73fa334c4..5d62292920d3 100644 --- a/packages/beacon-node/test/spec/presets/genesis.ts +++ b/packages/beacon-node/test/spec/presets/genesis.ts @@ -12,7 +12,7 @@ import {ForkName} from "@lodestar/params"; import {expectEqualBeaconState} from "../utils/expectEqualBeaconState.js"; import {TestRunnerFn} from "../utils/types.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; // The aim of the genesis tests is to provide a baseline to test genesis-state initialization and test if the // proposed genesis-validity conditions are working. diff --git a/packages/beacon-node/test/spec/presets/operations.ts b/packages/beacon-node/test/spec/presets/operations.ts index d3eb2f9683be..c0b38b9801d0 100644 --- a/packages/beacon-node/test/spec/presets/operations.ts +++ b/packages/beacon-node/test/spec/presets/operations.ts @@ -13,7 +13,7 @@ import {ForkName} from "@lodestar/params"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; import {BaseSpecTest, shouldVerify, TestRunnerFn} from "../utils/types.js"; /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/packages/beacon-node/test/spec/presets/rewards.ts b/packages/beacon-node/test/spec/presets/rewards.ts index 78147e4df2fb..4ae32731cc98 100644 --- a/packages/beacon-node/test/spec/presets/rewards.ts +++ b/packages/beacon-node/test/spec/presets/rewards.ts @@ -5,7 +5,7 @@ import {VectorCompositeType} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; import {TestRunnerFn} from "../utils/types.js"; import {assertCorrectProgressiveBalances} from "../config.js"; diff --git a/packages/beacon-node/test/spec/presets/sanity.ts b/packages/beacon-node/test/spec/presets/sanity.ts index f469ff8f67ba..61c559aaf3be 100644 --- a/packages/beacon-node/test/spec/presets/sanity.ts +++ b/packages/beacon-node/test/spec/presets/sanity.ts @@ -12,7 +12,7 @@ import {bnToNum} from "@lodestar/utils"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; import {shouldVerify, TestRunnerFn} from "../utils/types.js"; -import {getConfig} from "../utils/getConfig.js"; +import {getConfig} from "../../utils/config.js"; import {assertCorrectProgressiveBalances} from "../config.js"; /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/packages/beacon-node/test/spec/utils/getConfig.ts b/packages/beacon-node/test/spec/utils/getConfig.ts deleted file mode 100644 index 69f477607e32..000000000000 --- a/packages/beacon-node/test/spec/utils/getConfig.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {ForkName} from "@lodestar/params"; -import {config} from "@lodestar/config/default"; -import {IChainForkConfig, createIChainForkConfig} from "@lodestar/config"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export function getConfig(fork: ForkName, forkEpoch = 0): IChainForkConfig { - switch (fork) { - case ForkName.phase0: - return config; - case ForkName.altair: - return createIChainForkConfig({ALTAIR_FORK_EPOCH: forkEpoch}); - case ForkName.bellatrix: - return createIChainForkConfig({ - ALTAIR_FORK_EPOCH: 0, - BELLATRIX_FORK_EPOCH: forkEpoch, - }); - case ForkName.capella: - return createIChainForkConfig({ - ALTAIR_FORK_EPOCH: 0, - BELLATRIX_FORK_EPOCH: 0, - CAPELLA_FORK_EPOCH: forkEpoch, - }); - case ForkName.eip4844: - return createIChainForkConfig({ - ALTAIR_FORK_EPOCH: 0, - BELLATRIX_FORK_EPOCH: 0, - CAPELLA_FORK_EPOCH: 0, - EIP4844_FORK_EPOCH: forkEpoch, - }); - } -} diff --git a/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts index 5252098c436f..0b52f3f438c2 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts @@ -13,7 +13,7 @@ import { import {IBeaconChain} from "../../../../../../src/chain/index.js"; import {PERSIST_STATE_EVERY_EPOCHS} from "../../../../../../src/chain/archiver/archiveStates.js"; import {generateProtoBlock} from "../../../../../utils/block.js"; -import {generateCachedState, generateCachedStateWithPubkeys, generateState} from "../../../../../utils/state.js"; +import {generateCachedAltairState, generateCachedState, generateState} from "../../../../../utils/state.js"; import {StubbedBeaconDb} from "../../../../../utils/stub/index.js"; use(chaiAsPromised); @@ -222,7 +222,7 @@ describe("beacon state api utils", function () { }); describe("getStateValidatorIndex", async function () { - const state = await generateCachedStateWithPubkeys({}, config, true); + const state = generateCachedAltairState(); const pubkey2index = state.epochCtx.pubkey2index; it("should return valid: false on invalid input", () => { diff --git a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts index 29617bed1dd7..8d58ca49cafa 100644 --- a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts @@ -4,7 +4,6 @@ import sinon from "sinon"; import type {SecretKey} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; import {BitArray, fromHexString} from "@chainsafe/ssz"; -import {createIChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {ssz, phase0} from "@lodestar/types"; @@ -18,7 +17,7 @@ import { import {InsertOutcome} from "../../../../src/chain/opPools/types.js"; import {linspace} from "../../../../src/util/numpy.js"; import {generateAttestation, generateEmptyAttestation} from "../../../utils/attestation.js"; -import {generateCachedState} from "../../../utils/state.js"; +import {generateCachedAltairState} from "../../../utils/state.js"; import {renderBitArray} from "../../../utils/render.js"; import {ZERO_HASH_HEX} from "../../../../src/constants/constants.js"; import {generateEmptyProtoBlock} from "../../../utils/block.js"; @@ -33,9 +32,7 @@ describe("AggregatedAttestationPool", function () { const altairForkEpoch = 2020; const currentEpoch = altairForkEpoch + 10; const currentSlot = SLOTS_PER_EPOCH * currentEpoch; - // eslint-disable-next-line @typescript-eslint/naming-convention - const config = createIChainForkConfig(Object.assign({}, defaultChainConfig, {ALTAIR_FORK_EPOCH: altairForkEpoch})); - const originalState = generateCachedState({slot: currentSlot + 1}, config, true); + const originalState = generateCachedAltairState({slot: currentSlot + 1}, altairForkEpoch); let altairState: CachedBeaconStateAllForks; const attestation = generateAttestation({data: {slot: currentSlot, target: {epoch: currentEpoch}}}); const committee = [0, 1, 2, 3]; diff --git a/packages/beacon-node/test/unit/chain/validation/contributionAndProof.test.ts b/packages/beacon-node/test/unit/chain/validation/contributionAndProof.test.ts deleted file mode 100644 index 51d713dc22b3..000000000000 --- a/packages/beacon-node/test/unit/chain/validation/contributionAndProof.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import sinon from "sinon"; -import {SinonStubbedInstance} from "sinon"; -import {defaultChainConfig} from "@lodestar/config"; -import {BitArray} from "@chainsafe/ssz"; -import {SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; -import {createIChainForkConfig} from "@lodestar/config"; -import {BeaconChain} from "../../../../src/chain/index.js"; -import {LocalClock} from "../../../../src/chain/clock/index.js"; -import {SyncCommitteeErrorCode} from "../../../../src/chain/errors/syncCommitteeError.js"; -import {expectRejectedWithLodestarError} from "../../../utils/errors.js"; -import {generateSignedContributionAndProof} from "../../../utils/contributionAndProof.js"; -import {validateSyncCommitteeGossipContributionAndProof} from "../../../../src/chain/validation/syncCommitteeContributionAndProof.js"; -// eslint-disable-next-line no-restricted-imports -import * as syncCommitteeUtils from "../../../../../state-transition/src/util/aggregator.js"; -import {SinonStubFn} from "../../../utils/types.js"; -import {generateCachedStateWithPubkeys} from "../../../utils/state.js"; -import {SeenContributionAndProof} from "../../../../src/chain/seenCache/index.js"; -import {BlsVerifierMock} from "../../../utils/mocks/bls.js"; -import {StubbedChainMutable} from "../../../utils/stub/index.js"; - -type StubbedChain = StubbedChainMutable<"clock" | "bls">; - -// https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/p2p-interface.md -// TODO remove stub -describe.skip("Sync Committee Contribution And Proof validation", function () { - const sandbox = sinon.createSandbox(); - let chain: StubbedChain; - let clockStub: SinonStubbedInstance; - let isSyncCommitteeAggregatorStub: SinonStubFn; - - const altairForkEpoch = 2020; - const currentSlot = SLOTS_PER_EPOCH * (altairForkEpoch + 1); - // eslint-disable-next-line @typescript-eslint/naming-convention - const config = createIChainForkConfig(Object.assign({}, defaultChainConfig, {ALTAIR_FORK_EPOCH: altairForkEpoch})); - // all validators have same pubkey - const aggregatorIndex = 15; - - beforeEach(function () { - chain = sandbox.createStubInstance(BeaconChain); - (chain as { - seenContributionAndProof: SeenContributionAndProof; - }).seenContributionAndProof = new SeenContributionAndProof(null); - clockStub = sandbox.createStubInstance(LocalClock); - chain.clock = clockStub; - clockStub.isCurrentSlotGivenGossipDisparity.returns(true); - isSyncCommitteeAggregatorStub = sandbox.stub(syncCommitteeUtils, "isSyncCommitteeAggregator"); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it("should throw error - the signature's slot is not the current", async function () { - clockStub.isCurrentSlotGivenGossipDisparity.returns(false); - sandbox.stub(clockStub, "currentSlot").get(() => 100); - - const signedContributionAndProof = generateSignedContributionAndProof({contribution: {slot: 1}, aggregatorIndex}); - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.NOT_CURRENT_SLOT - ); - }); - - it("should throw error - subcommitteeIndex is not in allowed range", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot, subcommitteeIndex: 10000}, - aggregatorIndex, - }); - - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.INVALID_SUBCOMMITTEE_INDEX - ); - }); - - it("should throw error - same contribution data with superset of aggregationBits already known", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot}, - aggregatorIndex, - }); - const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); - chain.getHeadState.returns(headState); - chain.seenContributionAndProof.participantsKnown = () => true; - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN - ); - }); - - it("should throw error - there is same contribution with same aggregator and index and slot", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot}, - aggregatorIndex, - }); - const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); - chain.getHeadState.returns(headState); - chain.seenContributionAndProof.isAggregatorKnown = () => true; - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN - ); - }); - - it("should throw error - no participant", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot}, - aggregatorIndex, - }); - const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); - chain.getHeadState.returns(headState); - isSyncCommitteeAggregatorStub.returns(false); - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.NO_PARTICIPANT - ); - }); - - it("should throw error - invalid aggregator", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot, aggregationBits: BitArray.fromSingleBit(SYNC_COMMITTEE_SUBNET_SIZE, 0)}, - aggregatorIndex, - }); - const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); - chain.getHeadState.returns(headState); - isSyncCommitteeAggregatorStub.returns(false); - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.INVALID_AGGREGATOR - ); - }); - - /** - * Skip this spec: [REJECT] The aggregator's validator index is within the current sync committee -- i.e. state.validators[contribution_and_proof.aggregator_index].pubkey in state.current_sync_committee.pubkeys. - * because we check the aggregator index already and we always sync sync pubkeys with indices - */ - it("should throw error - aggregator index is not in sync committee", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot}, - aggregatorIndex: Infinity, - }); - isSyncCommitteeAggregatorStub.returns(true); - const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); - chain.getHeadState.returns(headState); - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.VALIDATOR_NOT_IN_SYNC_COMMITTEE - ); - }); - - it("should throw error - invalid selection_proof signature", async function () { - const signedContributionAndProof = generateSignedContributionAndProof({ - contribution: {slot: currentSlot, aggregationBits: BitArray.fromSingleBit(SYNC_COMMITTEE_SUBNET_SIZE, 0)}, - aggregatorIndex, - }); - isSyncCommitteeAggregatorStub.returns(true); - const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); - chain.getHeadState.returns(headState); - chain.bls = new BlsVerifierMock(false); - await expectRejectedWithLodestarError( - validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.INVALID_SIGNATURE - ); - }); - - // validation of signed_contribution_and_proof.signature is same test - // the validation of aggregated signature of aggregation_bits is the same test -}); diff --git a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts index ab9747a56e0d..f71795027068 100644 --- a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts @@ -8,7 +8,7 @@ import {LocalClock} from "../../../../src/chain/clock/index.js"; import {SyncCommitteeErrorCode} from "../../../../src/chain/errors/syncCommitteeError.js"; import {validateGossipSyncCommittee} from "../../../../src/chain/validation/syncCommittee.js"; import {expectRejectedWithLodestarError} from "../../../utils/errors.js"; -import {generateCachedState} from "../../../utils/state.js"; +import {generateCachedAltairState} from "../../../utils/state.js"; import {generateSyncCommitteeSignature} from "../../../utils/syncCommittee.js"; import {SeenSyncCommitteeMessages} from "../../../../src/chain/seenCache/index.js"; import {BlsVerifierMock} from "../../../utils/mocks/bls.js"; @@ -69,7 +69,7 @@ describe("Sync Committee Signature validation", function () { slot: currentSlot, validatorIndex: validatorIndexInSyncCommittee, }); - const headState = generateCachedState({slot: currentSlot}, config, true); + const headState = generateCachedAltairState({slot: currentSlot}, altairForkEpoch); chain.getHeadState.returns(headState); chain.seenSyncCommitteeMessages.isKnown = () => true; await expectRejectedWithLodestarError( @@ -80,7 +80,7 @@ describe("Sync Committee Signature validation", function () { it("should throw error - the validator is not part of the current sync committee", async function () { const syncCommittee = generateSyncCommitteeSignature({slot: currentSlot, validatorIndex: 100}); - const headState = generateCachedState({slot: currentSlot}, config, true); + const headState = generateCachedAltairState({slot: currentSlot}, altairForkEpoch); chain.getHeadState.returns(headState); await expectRejectedWithLodestarError( @@ -95,7 +95,7 @@ describe("Sync Committee Signature validation", function () { */ it.skip("should throw error - incorrect subnet", async function () { const syncCommittee = generateSyncCommitteeSignature({slot: currentSlot, validatorIndex: 1}); - const headState = generateCachedState({slot: currentSlot}, config, true); + const headState = generateCachedAltairState({slot: currentSlot}, altairForkEpoch); chain.getHeadState.returns(headState); await expectRejectedWithLodestarError( validateGossipSyncCommittee(chain, syncCommittee, 0), @@ -108,7 +108,7 @@ describe("Sync Committee Signature validation", function () { slot: currentSlot, validatorIndex: validatorIndexInSyncCommittee, }); - const headState = generateCachedState({slot: currentSlot}, config, true); + const headState = generateCachedAltairState({slot: currentSlot}, altairForkEpoch); chain.getHeadState.returns(headState); chain.bls = new BlsVerifierMock(false); diff --git a/packages/beacon-node/test/utils/block.ts b/packages/beacon-node/test/utils/block.ts index 50c0fc024a21..be8330ce6fa7 100644 --- a/packages/beacon-node/test/utils/block.ts +++ b/packages/beacon-node/test/utils/block.ts @@ -1,14 +1,10 @@ import deepmerge from "deepmerge"; -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 {Slot} from "@lodestar/types"; +import {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(slot: Slot = 0): phase0.BeaconBlock { return { @@ -40,39 +36,6 @@ export function generateEmptySignedBlock(slot: Slot = 0): phase0.SignedBeaconBlo }; } -export function generateEmptyReqRespBlockResponse(): ReqRespBlockResponse { - const block = generateEmptySignedBlock(); - - return { - type: EncodedPayloadType.bytes, - bytes: Buffer.from(ssz.phase0.SignedBeaconBlock.serialize(generateEmptySignedBlock())), - contextBytes: { - type: ContextBytesType.ForkDigest, - forkSlot: block.message.slot, - }, - }; -} - -export function blocksToReqRespBlockResponses( - blocks: allForks.SignedBeaconBlock[], - config?: IChainForkConfig -): ReqRespBlockResponse[] { - return blocks.map((block) => { - const slot = block.message.slot; - const sszType = config - ? config.getForkTypes(slot).SignedBeaconBlock - : defaultConfig.getForkTypes(slot).SignedBeaconBlock; - return { - type: EncodedPayloadType.bytes, - bytes: Buffer.from(sszType.serialize(block)), - contextBytes: { - type: ContextBytesType.ForkDigest, - forkSlot: block.message.slot, - }, - }; - }); -} - export function generateEmptySignedBlockHeader(): phase0.SignedBeaconBlockHeader { return { message: { diff --git a/packages/beacon-node/test/utils/config.ts b/packages/beacon-node/test/utils/config.ts index 9fd0797c47de..a1d381462380 100644 --- a/packages/beacon-node/test/utils/config.ts +++ b/packages/beacon-node/test/utils/config.ts @@ -1,6 +1,35 @@ import {config as chainConfig} from "@lodestar/config/default"; -import {createIBeaconConfig} from "@lodestar/config"; +import {createIBeaconConfig, IChainForkConfig, createIChainForkConfig} from "@lodestar/config"; +import {ForkName} from "@lodestar/params"; import {ZERO_HASH} from "../../src/constants/index.js"; /** default config with ZERO_HASH as genesisValidatorsRoot */ export const config = createIBeaconConfig(chainConfig, ZERO_HASH); + +/* eslint-disable @typescript-eslint/naming-convention */ +export function getConfig(fork: ForkName, forkEpoch = 0): IChainForkConfig { + switch (fork) { + case ForkName.phase0: + return config; + case ForkName.altair: + return createIChainForkConfig({ALTAIR_FORK_EPOCH: forkEpoch}); + case ForkName.bellatrix: + return createIChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: forkEpoch, + }); + case ForkName.capella: + return createIChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: forkEpoch, + }); + case ForkName.eip4844: + return createIChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + EIP4844_FORK_EPOCH: forkEpoch, + }); + } +} diff --git a/packages/beacon-node/test/utils/mocks/chain/chain.ts b/packages/beacon-node/test/utils/mocks/chain/chain.ts index cdea4edd6b42..4a75a77f5fec 100644 --- a/packages/beacon-node/test/utils/mocks/chain/chain.ts +++ b/packages/beacon-node/test/utils/mocks/chain/chain.ts @@ -104,7 +104,7 @@ export class MockBeaconChain implements IBeaconChain { }); readonly checkpointBalancesCache = new CheckpointBalancesCache(); - private state: BeaconStateAllForks; + private readonly state: CachedBeaconStateAllForks; private abortController: AbortController; constructor({genesisTime, chainId, networkId, state, config}: IMockChainParams) { @@ -114,7 +114,7 @@ export class MockBeaconChain implements IBeaconChain { this.bls = sinon.createStubInstance(BlsSingleThreadVerifier); this.chainId = chainId || 0; this.networkId = networkId || BigInt(0); - this.state = state; + this.state = createCachedBeaconStateTest(state, config); this.anchorStateLatestBlockSlot = state.latestBlockHeader.slot; this.config = config; this.emitter = new ChainEventEmitter(); @@ -158,11 +158,11 @@ export class MockBeaconChain implements IBeaconChain { persistInvalidSszView(_: TreeView): void {} getHeadState(): CachedBeaconStateAllForks { - return createCachedBeaconStateTest(this.state, this.config); + return this.state; } async getHeadStateAtCurrentEpoch(): Promise { - return createCachedBeaconStateTest(this.state, this.config); + return this.state; } async getCanonicalBlockAtSlot(slot: Slot): Promise { diff --git a/packages/beacon-node/test/utils/state.ts b/packages/beacon-node/test/utils/state.ts index afa1f16e6bc2..71ffedd4bbc7 100644 --- a/packages/beacon-node/test/utils/state.ts +++ b/packages/beacon-node/test/utils/state.ts @@ -7,23 +7,14 @@ import { CachedBeaconStateBellatrix, BeaconStateBellatrix, } from "@lodestar/state-transition"; -import {BitArray} from "@chainsafe/ssz"; -import {phase0, allForks, altair, ssz} from "@lodestar/types"; -import {createIBeaconConfig} from "@lodestar/config"; -import { - EPOCHS_PER_HISTORICAL_VECTOR, - EPOCHS_PER_SLASHINGS_VECTOR, - FAR_FUTURE_EPOCH, - ForkSeq, - MAX_EFFECTIVE_BALANCE, - SLOTS_PER_HISTORICAL_ROOT, - SYNC_COMMITTEE_SIZE, -} from "@lodestar/params"; +import {allForks, altair, bellatrix, ssz} from "@lodestar/types"; +import {createIBeaconConfig, IChainForkConfig} from "@lodestar/config"; +import {FAR_FUTURE_EPOCH, ForkName, ForkSeq, MAX_EFFECTIVE_BALANCE, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; import bls from "@chainsafe/bls"; -import {GENESIS_EPOCH, GENESIS_SLOT, ZERO_HASH} from "../../src/constants/index.js"; import {generateEmptyBlock} from "./block.js"; import {generateValidator, generateValidators} from "./validator.js"; +import {getConfig} from "./config.js"; /** * Copy of BeaconState, but all fields are marked optional to allow for swapping out variables as needed. @@ -41,10 +32,16 @@ type TestBeaconState = Partial; */ export function generateState( opts: TestBeaconState = {}, - config = minimalConfig, - forkSeq: ForkSeq = ForkSeq.phase0, + config: IChainForkConfig = minimalConfig, withPubkey = false ): BeaconStateAllForks { + const stateSlot = opts.slot ?? 0; + const state = config.getForkTypes(stateSlot).BeaconState.defaultValue(); + const forkSeq = config.getForkSeq(stateSlot); + + // Mutate state adding properties + Object.assign(state, opts); + const validatorOpts = { activationEpoch: 0, effectiveBalance: MAX_EFFECTIVE_BALANCE, @@ -52,6 +49,7 @@ export function generateState( exitEpoch: FAR_FUTURE_EPOCH, }; const numValidators = 16; + const validators = withPubkey ? Array.from({length: numValidators}, (_, i) => { const sk = bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1)); @@ -61,110 +59,45 @@ export function generateState( }); }) : generateValidators(numValidators, validatorOpts); - const defaultState: phase0.BeaconState = { - genesisTime: Math.floor(Date.now() / 1000), - genesisValidatorsRoot: ZERO_HASH, - slot: GENESIS_SLOT, - fork: { - previousVersion: config.GENESIS_FORK_VERSION, - currentVersion: config.GENESIS_FORK_VERSION, - epoch: GENESIS_EPOCH, - }, - latestBlockHeader: { - slot: 0, - proposerIndex: 0, - parentRoot: Buffer.alloc(32), - stateRoot: Buffer.alloc(32), - bodyRoot: ssz.phase0.BeaconBlockBody.hashTreeRoot(generateEmptyBlock().body), - }, - blockRoots: Array.from({length: SLOTS_PER_HISTORICAL_ROOT}, () => ZERO_HASH), - stateRoots: Array.from({length: SLOTS_PER_HISTORICAL_ROOT}, () => ZERO_HASH), - historicalRoots: [], - eth1Data: { - depositRoot: Buffer.alloc(32), - blockHash: Buffer.alloc(32), - depositCount: 0, - }, - eth1DataVotes: [], - eth1DepositIndex: 0, - validators: validators, - balances: Array.from({length: numValidators}, () => MAX_EFFECTIVE_BALANCE), - randaoMixes: Array.from({length: EPOCHS_PER_HISTORICAL_VECTOR}, () => ZERO_HASH), - slashings: Array.from({length: EPOCHS_PER_SLASHINGS_VECTOR}, () => BigInt(0)), - previousEpochAttestations: [], - currentEpochAttestations: [], - justificationBits: BitArray.fromBitLen(4), - previousJustifiedCheckpoint: { - epoch: GENESIS_EPOCH, - root: ZERO_HASH, - }, - currentJustifiedCheckpoint: { - epoch: GENESIS_EPOCH, - root: ZERO_HASH, - }, - finalizedCheckpoint: { - epoch: GENESIS_EPOCH, - root: ZERO_HASH, - }, - ...opts, - }; - if (forkSeq === ForkSeq.phase0) { - return ssz.phase0.BeaconState.toViewDU(defaultState); - } + state.genesisTime = Math.floor(Date.now() / 1000); + state.fork.previousVersion = config.GENESIS_FORK_VERSION; + state.fork.currentVersion = config.GENESIS_FORK_VERSION; + state.latestBlockHeader.bodyRoot = ssz.phase0.BeaconBlockBody.hashTreeRoot(generateEmptyBlock().body); + state.validators = validators; + state.balances = Array.from({length: numValidators}, () => MAX_EFFECTIVE_BALANCE); - const defaultAltairState: altair.BeaconState = { - ...ssz.altair.BeaconState.defaultValue(), - ...defaultState, - previousEpochParticipation: [...[0xff, 0xff], ...Array.from({length: numValidators - 2}, () => 0)], - currentEpochParticipation: [...[0xff, 0xff], ...Array.from({length: numValidators - 2}, () => 0)], - currentSyncCommittee: { + if (forkSeq >= ForkSeq.altair) { + const stateAltair = state as altair.BeaconState; + stateAltair.previousEpochParticipation = [...[0xff, 0xff], ...Array.from({length: numValidators - 2}, () => 0)]; + stateAltair.currentEpochParticipation = [...[0xff, 0xff], ...Array.from({length: numValidators - 2}, () => 0)]; + stateAltair.currentSyncCommittee = { pubkeys: Array.from({length: SYNC_COMMITTEE_SIZE}, (_, i) => validators[i % validators.length].pubkey), aggregatePubkey: ssz.BLSPubkey.defaultValue(), - }, - nextSyncCommittee: { + }; + stateAltair.nextSyncCommittee = { pubkeys: Array.from({length: SYNC_COMMITTEE_SIZE}, (_, i) => validators[i % validators.length].pubkey), aggregatePubkey: ssz.BLSPubkey.defaultValue(), - }, - }; - - if (forkSeq === ForkSeq.altair) { - return ssz.altair.BeaconState.toViewDU(defaultAltairState); - } - - const defaultBellatrixState = { - ...defaultAltairState, - latestExecutionPayloadHeader: {...ssz.bellatrix.ExecutionPayloadHeader.defaultValue(), blockNumber: 2022}, - }; - - // Bellatrix upwards - if (forkSeq === ForkSeq.bellatrix) { - return ssz.bellatrix.BeaconState.toViewDU(defaultBellatrixState); + }; } - const defaultCapellaState = { - ...defaultAltairState, - latestExecutionPayloadHeader: {...ssz.capella.ExecutionPayloadHeader.defaultValue(), blockNumber: 2022}, - nextWithdrawalIndex: 0, - nextWithdrawalValidatorIndex: 0, - }; - - if (forkSeq === ForkSeq.capella) { - return ssz.capella.BeaconState.toViewDU(defaultCapellaState); + if (forkSeq >= ForkSeq.bellatrix) { + const stateBellatrix = state as bellatrix.BeaconState; + stateBellatrix.latestExecutionPayloadHeader = { + ...ssz.bellatrix.ExecutionPayloadHeader.defaultValue(), + blockNumber: 2022, + }; } - throw Error(`fork with seq=${forkSeq} not implemented`); + return config.getForkTypes(stateSlot).BeaconState.toViewDU(state); } /** * This generates state with default pubkey */ -export function generateCachedState( - opts: TestBeaconState = {}, - config = minimalConfig, - isAltair = false -): CachedBeaconStateAllForks { - const state = generateState(opts, config, isAltair ? ForkSeq.altair : ForkSeq.phase0); +export function generateCachedState(opts?: TestBeaconState): CachedBeaconStateAllForks { + const config = getConfig(ForkName.phase0); + const state = generateState(opts, config); return createCachedBeaconState(state, { config: createIBeaconConfig(config, state.genesisValidatorsRoot), // This is a performance test, there's no need to have a global shared cache of keys @@ -176,11 +109,9 @@ export function generateCachedState( /** * This generates state with default pubkey */ -export function generateCachedAltairState( - opts: TestBeaconState = {}, - config = minimalConfig -): CachedBeaconStateAllForks { - const state = generateState(opts, config, ForkSeq.altair); +export function generateCachedAltairState(opts?: TestBeaconState, altairForkEpoch = 0): CachedBeaconStateAllForks { + const config = getConfig(ForkName.altair, altairForkEpoch); + const state = generateState(opts, config); return createCachedBeaconState(state, { config: createIBeaconConfig(config, state.genesisValidatorsRoot), // This is a performance test, there's no need to have a global shared cache of keys @@ -192,11 +123,9 @@ export function generateCachedAltairState( /** * This generates state with default pubkey */ -export function generateCachedBellatrixState( - opts: TestBeaconState = {}, - config = minimalConfig -): CachedBeaconStateBellatrix { - const state = generateState(opts, config, ForkSeq.bellatrix); +export function generateCachedBellatrixState(opts?: TestBeaconState): CachedBeaconStateBellatrix { + const config = getConfig(ForkName.bellatrix); + const state = generateState(opts, config); return createCachedBeaconState(state as BeaconStateBellatrix, { config: createIBeaconConfig(config, state.genesisValidatorsRoot), // This is a performance test, there's no need to have a global shared cache of keys @@ -204,14 +133,3 @@ export function generateCachedBellatrixState( index2pubkey: [], }); } - -/** - * This generates state with real pubkey - */ -export async function generateCachedStateWithPubkeys( - opts: TestBeaconState = {}, - config = minimalConfig, - isAltair = false -): Promise { - return generateCachedState(opts, config, isAltair); -} diff --git a/packages/reqresp/src/ReqResp.ts b/packages/reqresp/src/ReqResp.ts index 5199dc6092e4..6b8e725929fe 100644 --- a/packages/reqresp/src/ReqResp.ts +++ b/packages/reqresp/src/ReqResp.ts @@ -25,6 +25,10 @@ export interface ReqRespOpts extends SendRequestOpts { getPeerLogMetadata?: (peerId: string) => string; } +export interface ReqRespRegisterOpts { + ignoreIfDuplicate?: boolean; +} + /** * Implementation of Ethereum Consensus p2p Req/Resp domain. * For the spec that this code is based on, see: @@ -33,7 +37,7 @@ export interface ReqRespOpts extends SendRequestOpts { */ export class ReqResp { private readonly libp2p: Libp2p; - private readonly logger: ILogger; + protected readonly logger: ILogger; private readonly metrics: Metrics | null; private controller = new AbortController(); /** Tracks request and responses in a sequential counter */ @@ -41,7 +45,7 @@ export class ReqResp { private readonly protocolPrefix: string; /** `${protocolPrefix}/${method}/${version}/${encoding}` */ - private readonly supportedProtocols = new Map(); + private readonly registeredProtocols = new Map(); constructor(modules: ReqRespProtocolModules, private readonly opts: ReqRespOpts = {}) { this.libp2p = modules.libp2p; @@ -50,10 +54,51 @@ export class ReqResp { this.protocolPrefix = opts.protocolPrefix ?? DEFAULT_PROTOCOL_PREFIX; } - registerProtocol(protocol: ProtocolDefinition): void { - const {method, version, encoding} = protocol; - const protocolID = this.formatProtocolID(method, version, encoding); - this.supportedProtocols.set(protocolID, protocol as ProtocolDefinition); + /** + * Register protocol as supported and to libp2p. + * async because libp2p registar persists the new protocol list in the peer-store. + * Throws if the same protocol is registered twice. + * Can be called at any time, no concept of started / stopped + */ + async registerProtocol( + protocol: ProtocolDefinition, + opts?: ReqRespRegisterOpts + ): Promise { + const protocolID = this.formatProtocolID(protocol); + + // libp2p will throw on error on duplicates, allow to overwrite behaviour + if (opts?.ignoreIfDuplicate && this.registeredProtocols.has(protocolID)) { + return; + } + + this.registeredProtocols.set(protocolID, protocol as ProtocolDefinition); + + return this.libp2p.handle(protocolID, this.getRequestHandler(protocol)); + } + + /** + * Remove protocol as supported and from libp2p. + * async because libp2p registar persists the new protocol list in the peer-store. + * Does NOT throw if the protocolID is unknown. + * Can be called at any time, no concept of started / stopped + */ + async unregisterProtocol(protocolID: ProtocolID): Promise { + this.registeredProtocols.delete(protocolID); + + return this.libp2p.unhandle(protocolID); + } + + /** + * Remove all registered protocols from libp2p + */ + async unregisterAllProtocols(): Promise { + for (const protocolID of this.registeredProtocols.keys()) { + await this.unregisterProtocol(protocolID); + } + } + + getRegisteredProtocols(): ProtocolID[] { + return Array.from(this.registeredProtocols.values()).map((protocol) => this.formatProtocolID(protocol)); } async start(): Promise { @@ -61,16 +106,9 @@ export class ReqResp { // 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(); } @@ -90,8 +128,8 @@ export class ReqResp { const protocolIDs: string[] = []; for (const version of versions) { - const protocolID = this.formatProtocolID(method, version, encoding); - const protocol = this.supportedProtocols.get(protocolID); + const protocolID = this.formatProtocolID({method, version, encoding}); + const protocol = this.registeredProtocols.get(protocolID); if (!protocol) { throw Error(`Request to send to protocol ${protocolID} but it has not been declared`); } @@ -175,7 +213,7 @@ 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 formatProtocolID(this.protocolPrefix, method, version, encoding); + protected formatProtocolID(protocol: Pick): string { + return formatProtocolID(this.protocolPrefix, protocol.method, protocol.version, protocol.encoding); } }