Skip to content

Commit

Permalink
feat: add cli flag to disable light client server (#6913)
Browse files Browse the repository at this point in the history
* Add disable flag

* Add util function to check lightclient server

* Add reqresp checks

* Add api checks

* Fix beacon-node usage of lightclient server

* Fix types

* Fix the code with feedback

* Disable gossipsub topic for lightclient

* Fix the types

* Update the type checks

* Clean reassingment

* Update code
  • Loading branch information
nazarhussain authored Jul 18, 2024
1 parent c6369ee commit c78b6ce
Show file tree
Hide file tree
Showing 24 changed files with 106 additions and 29 deletions.
20 changes: 15 additions & 5 deletions packages/beacon-node/src/api/impl/lightclient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {routes} from "@lodestar/api";
import {ApplicationMethods} from "@lodestar/api/server";
import {MAX_REQUEST_LIGHT_CLIENT_UPDATES, MAX_REQUEST_LIGHT_CLIENT_COMMITTEE_HASHES} from "@lodestar/params";
import {ApiModules} from "../types.js";

import {assertLightClientServer} from "../../../node/utils/lightclient.js";
// TODO: Import from lightclient/server package

export function getLightclientApi({
Expand All @@ -12,16 +12,21 @@ export function getLightclientApi({
}: Pick<ApiModules, "chain" | "config">): ApplicationMethods<routes.lightclient.Endpoints> {
return {
async getLightClientUpdatesByRange({startPeriod, count}) {
const lightClientServer = chain.lightClientServer;
assertLightClientServer(lightClientServer);

const maxAllowedCount = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, count);
const periods = Array.from({length: maxAllowedCount}, (_ignored, i) => i + startPeriod);
const updates = await Promise.all(periods.map((period) => chain.lightClientServer.getUpdate(period)));
const updates = await Promise.all(periods.map((period) => lightClientServer.getUpdate(period)));
return {
data: updates,
meta: {versions: updates.map((update) => config.getForkName(update.attestedHeader.beacon.slot))},
};
},

async getLightClientOptimisticUpdate() {
assertLightClientServer(chain.lightClientServer);

const update = chain.lightClientServer.getOptimisticUpdate();
if (update === null) {
throw Error("No optimistic update available");
Expand All @@ -30,6 +35,8 @@ export function getLightclientApi({
},

async getLightClientFinalityUpdate() {
assertLightClientServer(chain.lightClientServer);

const update = chain.lightClientServer.getFinalityUpdate();
if (update === null) {
throw Error("No finality update available");
Expand All @@ -38,16 +45,19 @@ export function getLightclientApi({
},

async getLightClientBootstrap({blockRoot}) {
assertLightClientServer(chain.lightClientServer);

const bootstrapProof = await chain.lightClientServer.getBootstrap(fromHex(blockRoot));
return {data: bootstrapProof, meta: {version: config.getForkName(bootstrapProof.header.beacon.slot)}};
},

async getLightClientCommitteeRoot({startPeriod, count}) {
const lightClientServer = chain.lightClientServer;
assertLightClientServer(lightClientServer);

const maxAllowedCount = Math.min(MAX_REQUEST_LIGHT_CLIENT_COMMITTEE_HASHES, count);
const periods = Array.from({length: maxAllowedCount}, (_ignored, i) => i + startPeriod);
const committeeHashes = await Promise.all(
periods.map((period) => chain.lightClientServer.getCommitteeRoot(period))
);
const committeeHashes = await Promise.all(periods.map((period) => lightClientServer.getCommitteeRoot(period)));
return {data: committeeHashes};
},
};
Expand Down
6 changes: 4 additions & 2 deletions packages/beacon-node/src/chain/archiver/archiveBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function archiveBlocks(
config: ChainForkConfig,
db: IBeaconDb,
forkChoice: IForkChoice,
lightclientServer: LightClientServer,
lightclientServer: LightClientServer | undefined,
logger: Logger,
finalizedCheckpoint: CheckpointHex,
currentEpoch: Epoch,
Expand Down Expand Up @@ -111,7 +111,9 @@ export async function archiveBlocks(
nonCheckpointBlockRoots.push(block.root);
}

await lightclientServer.pruneNonCheckpointData(nonCheckpointBlockRoots);
if (lightclientServer) {
await lightclientServer.pruneNonCheckpointData(nonCheckpointBlockRoots);
}

logger.verbose("Archiving of finalized blocks complete", {
totalArchived: finalizedCanonicalBlocks.length,
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export async function importBlock(
// we want to import block asap so do this in the next event loop
callInNextEventLoop(() => {
try {
this.lightClientServer.onImportBlockHead(
this.lightClientServer?.onImportBlockHead(
block.message as BeaconBlock<ForkLightClient>,
postState as CachedBeaconStateAltair,
parentBlockSlot
Expand Down
7 changes: 4 additions & 3 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class BeaconChain implements IBeaconChain {
readonly clock: IClock;
readonly emitter: ChainEventEmitter;
readonly regen: QueuedStateRegenerator;
readonly lightClientServer: LightClientServer;
readonly lightClientServer?: LightClientServer;
readonly reprocessController: ReprocessController;

// Ops pool
Expand Down Expand Up @@ -305,7 +305,9 @@ export class BeaconChain implements IBeaconChain {
signal,
});

const lightClientServer = new LightClientServer(opts, {config, db, metrics, emitter, logger});
if (!opts.disableLightClientServer) {
this.lightClientServer = new LightClientServer(opts, {config, db, metrics, emitter, logger});
}

this.reprocessController = new ReprocessController(this.metrics);

Expand All @@ -316,7 +318,6 @@ export class BeaconChain implements IBeaconChain {
this.regen = regen;
this.bls = bls;
this.emitter = emitter;
this.lightClientServer = lightClientServer;

this.archiver = new Archiver(db, this, logger, signal, opts);
// always run PrepareNextSlotScheduler except for fork_choice spec tests
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export interface IBeaconChain {
readonly clock: IClock;
readonly emitter: ChainEventEmitter;
readonly regen: IStateRegenerator;
readonly lightClientServer: LightClientServer;
readonly lightClientServer?: LightClientServer;
readonly reprocessController: ReprocessController;
readonly pubkey2index: PubkeyIndexMap;
readonly index2pubkey: Index2PubkeyCache;
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/lightClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {

export type LightClientServerOpts = {
disableLightClientServerOnImportBlockHead?: boolean;
disableLightClientServer?: boolean;
};

type DependentRootHex = RootHex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {LightClientFinalityUpdate} from "@lodestar/types";
import {IBeaconChain} from "../interface.js";
import {LightClientError, LightClientErrorCode} from "../errors/lightClientError.js";
import {GossipAction} from "../errors/index.js";
import {assertLightClientServer} from "../../node/utils/lightclient.js";
import {updateReceivedTooEarly} from "./lightClientOptimisticUpdate.js";

// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#light_client_finality_update
Expand All @@ -11,6 +12,8 @@ export function validateLightClientFinalityUpdate(
chain: IBeaconChain,
gossipedFinalityUpdate: LightClientFinalityUpdate
): void {
assertLightClientServer(chain.lightClientServer);

// [IGNORE] No other finality_update with a lower or equal finalized_header.slot was already forwarded on the network
const gossipedFinalitySlot = gossipedFinalityUpdate.finalizedHeader.beacon.slot;
const localFinalityUpdate = chain.lightClientServer.getFinalityUpdate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {IBeaconChain} from "../interface.js";
import {LightClientError, LightClientErrorCode} from "../errors/lightClientError.js";
import {GossipAction} from "../errors/index.js";
import {MAXIMUM_GOSSIP_CLOCK_DISPARITY} from "../../constants/index.js";
import {assertLightClientServer} from "../../node/utils/lightclient.js";

// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#light_client_optimistic_update
export function validateLightClientOptimisticUpdate(
config: ChainForkConfig,
chain: IBeaconChain,
gossipedOptimisticUpdate: LightClientOptimisticUpdate
): void {
assertLightClientServer(chain.lightClientServer);

// [IGNORE] No other optimistic_update with a lower or equal attested_header.slot was already forwarded on the network
const gossipedAttestedSlot = gossipedOptimisticUpdate.attestedHeader.beacon.slot;
const localOptimisticUpdate = chain.lightClientServer.getOptimisticUpdate();
Expand Down
14 changes: 10 additions & 4 deletions packages/beacon-node/src/network/core/networkCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,19 +499,25 @@ export class NetworkCore implements INetworkCore {
private subscribeCoreTopicsAtFork(fork: ForkName): void {
if (this.subscribedForks.has(fork)) return;
this.subscribedForks.add(fork);
const {subscribeAllSubnets} = this.opts;
const {subscribeAllSubnets, disableLightClientServer} = this.opts;

for (const topic of getCoreTopicsAtFork(fork, {subscribeAllSubnets})) {
for (const topic of getCoreTopicsAtFork(fork, {
subscribeAllSubnets,
disableLightClientServer,
})) {
this.gossip.subscribeTopic({...topic, fork});
}
}

private unsubscribeCoreTopicsAtFork(fork: ForkName): void {
if (!this.subscribedForks.has(fork)) return;
this.subscribedForks.delete(fork);
const {subscribeAllSubnets} = this.opts;
const {subscribeAllSubnets, disableLightClientServer} = this.opts;

for (const topic of getCoreTopicsAtFork(fork, {subscribeAllSubnets})) {
for (const topic of getCoreTopicsAtFork(fork, {
subscribeAllSubnets,
disableLightClientServer,
})) {
this.gossip.unsubscribeTopic({...topic, fork});
}
}
Expand Down
12 changes: 9 additions & 3 deletions packages/beacon-node/src/network/gossip/gossipsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type Eth2GossipsubOpts = {
gossipsubAwaitHandler?: boolean;
disableFloodPublish?: boolean;
skipParamsLog?: boolean;
disableLightClientServer?: boolean;
};

/**
Expand Down Expand Up @@ -124,7 +125,9 @@ export class Eth2Gossipsub extends GossipSub {
isFinite(config.BELLATRIX_FORK_EPOCH) ? GOSSIP_MAX_SIZE_BELLATRIX : GOSSIP_MAX_SIZE
),
metricsRegister: metricsRegister as MetricsRegister | null,
metricsTopicStrToLabel: metricsRegister ? getMetricsTopicStrToLabel(config) : undefined,
metricsTopicStrToLabel: metricsRegister
? getMetricsTopicStrToLabel(config, {disableLightClientServer: opts.disableLightClientServer ?? false})
: undefined,
asyncValidation: true,

maxOutboundBufferSize: MAX_OUTBOUND_BUFFER_SIZE,
Expand Down Expand Up @@ -321,11 +324,14 @@ function attSubnetLabel(subnet: number): string {
else return `0${subnet}`;
}

function getMetricsTopicStrToLabel(config: BeaconConfig): TopicStrToLabel {
function getMetricsTopicStrToLabel(config: BeaconConfig, opts: {disableLightClientServer: boolean}): TopicStrToLabel {
const metricsTopicStrToLabel = new Map<TopicStr, TopicLabel>();

for (const {name: fork} of config.forksAscendingEpochOrder) {
const topics = getCoreTopicsAtFork(fork, {subscribeAllSubnets: true});
const topics = getCoreTopicsAtFork(fork, {
subscribeAllSubnets: true,
disableLightClientServer: opts.disableLightClientServer,
});
for (const topic of topics) {
metricsTopicStrToLabel.set(stringifyGossipTopic(config, {...topic, fork}), topic.type);
}
Expand Down
8 changes: 5 additions & 3 deletions packages/beacon-node/src/network/gossip/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr:
*/
export function getCoreTopicsAtFork(
fork: ForkName,
opts: {subscribeAllSubnets?: boolean}
opts: {subscribeAllSubnets?: boolean; disableLightClientServer?: boolean}
): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
// Common topics for all forks
const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [
Expand All @@ -227,8 +227,10 @@ export function getCoreTopicsAtFork(
// Any fork after altair included
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 (!opts.disableLightClientServer) {
topics.push({type: GossipType.light_client_optimistic_update});
topics.push({type: GossipType.light_client_finality_update});
}
}

if (opts.subscribeAllSubnets) {
Expand Down
7 changes: 5 additions & 2 deletions packages/beacon-node/src/network/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {SubnetsServiceOpts} from "./subnets/interface.js";
export interface NetworkOptions
extends PeerManagerOpts,
// remove all Functions
Omit<ReqRespBeaconNodeOpts, "getPeerLogMetadata" | "onRateLimit">,
Omit<ReqRespBeaconNodeOpts, "getPeerLogMetadata" | "onRateLimit" | "disableLightClientServer">,
NetworkProcessorOpts,
PeerRpcScoreOpts,
SubnetsServiceOpts,
Eth2GossipsubOpts {
Omit<Eth2GossipsubOpts, "disableLightClientServer"> {
localMultiaddrs: string[];
bootMultiaddrs?: string[];
subscribeAllSubnets?: boolean;
Expand All @@ -22,6 +22,7 @@ export interface NetworkOptions
private?: boolean;
useWorker?: boolean;
maxYoungGenerationSizeMb?: number;
disableLightClientServer?: boolean;
}

export const defaultNetworkOptions: NetworkOptions = {
Expand All @@ -41,4 +42,6 @@ export const defaultNetworkOptions: NetworkOptions = {
slotsToSubscribeBeforeAggregatorDuty: 2,
// this should only be set to true if useWorker is true
beaconAttestationBatchValidation: true,
// This will enable the light client server by default
disableLightClientServer: false,
};
6 changes: 4 additions & 2 deletions packages/beacon-node/src/network/reqresp/ReqRespBeaconNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export interface ReqRespBeaconNodeModules {
getHandler: GetReqRespHandlerFn;
}

export type ReqRespBeaconNodeOpts = ReqRespOpts;
export type ReqRespBeaconNodeOpts = ReqRespOpts & {disableLightClientServer?: boolean};

/**
* Implementation of Ethereum Consensus p2p Req/Resp domain.
Expand All @@ -72,6 +72,7 @@ export class ReqRespBeaconNode extends ReqResp {

private readonly config: BeaconConfig;
protected readonly logger: Logger;
protected readonly disableLightClientServer: boolean;

constructor(modules: ReqRespBeaconNodeModules, options: ReqRespBeaconNodeOpts = {}) {
const {events, peersData, peerRpcScores, metadata, metrics, logger} = modules;
Expand All @@ -95,6 +96,7 @@ export class ReqRespBeaconNode extends ReqResp {
}
);

this.disableLightClientServer = options.disableLightClientServer ?? false;
this.peerRpcScores = peerRpcScores;
this.peersData = peersData;
this.config = modules.config;
Expand Down Expand Up @@ -233,7 +235,7 @@ export class ReqRespBeaconNode extends ReqResp {
);
}

if (ForkSeq[fork] >= ForkSeq.altair) {
if (ForkSeq[fork] >= ForkSeq.altair && !this.disableLightClientServer) {
// Should be okay to enable before altair, but for consistency only enable afterwards
protocolsAtFork.push(
[protocols.LightClientBootstrap(this.config), this.getHandler(ReqRespMethod.LightClientBootstrap)],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import {
import {Root} from "@lodestar/types";
import {IBeaconChain} from "../../../chain/index.js";
import {ReqRespMethod, responseSszTypeByMethod} from "../types.js";
import {assertLightClientServer} from "../../../node/utils/lightclient.js";

export async function* onLightClientBootstrap(requestBody: Root, chain: IBeaconChain): AsyncIterable<ResponseOutgoing> {
assertLightClientServer(chain.lightClientServer);

try {
const bootstrap = await chain.lightClientServer.getBootstrap(requestBody);
const fork = chain.config.getForkName(bootstrap.header.beacon.slot);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {ResponseOutgoing, RespStatus, ResponseError} from "@lodestar/reqresp";
import {IBeaconChain} from "../../../chain/index.js";
import {ReqRespMethod, responseSszTypeByMethod} from "../types.js";
import {assertLightClientServer} from "../../../node/utils/lightclient.js";

export async function* onLightClientFinalityUpdate(chain: IBeaconChain): AsyncIterable<ResponseOutgoing> {
assertLightClientServer(chain.lightClientServer);

const update = chain.lightClientServer.getFinalityUpdate();
if (update === null) {
throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest finality update available");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {ResponseOutgoing, ResponseError, RespStatus} from "@lodestar/reqresp";
import {IBeaconChain} from "../../../chain/index.js";
import {ReqRespMethod, responseSszTypeByMethod} from "../types.js";
import {assertLightClientServer} from "../../../node/utils/lightclient.js";

export async function* onLightClientOptimisticUpdate(chain: IBeaconChain): AsyncIterable<ResponseOutgoing> {
assertLightClientServer(chain.lightClientServer);

const update = chain.lightClientServer.getOptimisticUpdate();
if (update === null) {
throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "No latest optimistic update available");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {
} from "@lodestar/reqresp";
import {IBeaconChain} from "../../../chain/index.js";
import {ReqRespMethod, responseSszTypeByMethod} from "../types.js";
import {assertLightClientServer} from "../../../node/utils/lightclient.js";

export async function* onLightClientUpdatesByRange(
requestBody: altair.LightClientUpdatesByRange,
chain: IBeaconChain
): AsyncIterable<ResponseOutgoing> {
assertLightClientServer(chain.lightClientServer);

const count = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, requestBody.count);
for (let period = requestBody.startPeriod; period < requestBody.startPeriod + count; period++) {
try {
Expand Down
7 changes: 7 additions & 0 deletions packages/beacon-node/src/node/utils/lightclient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {LightClientServer} from "../../chain/lightClient/index.js";

export function assertLightClientServer(server: LightClientServer | undefined): asserts server is LightClientServer {
if (!server) {
throw Error("Light client server is disabled");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {describe, it, expect, beforeEach, afterEach, vi} from "vitest";
import {createChainForkConfig, defaultChainConfig} from "@lodestar/config";
import {altair, ssz} from "@lodestar/types";
import {computeTimeAtSlot} from "@lodestar/state-transition";
import {RequiredSelective} from "@lodestar/utils";
import {validateLightClientFinalityUpdate} from "../../../../src/chain/validation/lightClientFinalityUpdate.js";
import {LightClientErrorCode} from "../../../../src/chain/errors/lightClientError.js";
import {IBeaconChain} from "../../../../src/chain/index.js";
Expand Down Expand Up @@ -30,7 +31,7 @@ describe("Light Client Finality Update validation", function () {
}
});

function mockChain(): IBeaconChain {
function mockChain(): RequiredSelective<IBeaconChain, "lightClientServer"> {
const chain = getMockedBeaconChain();
vi.spyOn(chain, "genesisTime", "get").mockReturnValue(0);
return chain;
Expand Down
Loading

0 comments on commit c78b6ce

Please sign in to comment.