diff --git a/packages/api/src/beacon/client/index.ts b/packages/api/src/beacon/client/index.ts index 9601021108be..74d80375ba97 100644 --- a/packages/api/src/beacon/client/index.ts +++ b/packages/api/src/beacon/client/index.ts @@ -9,6 +9,7 @@ import * as events from "./events.js"; import * as lightclient from "./lightclient.js"; import * as lodestar from "./lodestar.js"; import * as node from "./node.js"; +import * as proof from "./proof.js"; import * as validator from "./validator.js"; type ClientModules = HttpClientModules & { @@ -31,6 +32,7 @@ export function getClient(opts: HttpClientOptions, modules: ClientModules): Api lightclient: lightclient.getClient(config, httpClient), lodestar: lodestar.getClient(config, httpClient), node: node.getClient(config, httpClient), + proof: proof.getClient(config, httpClient), validator: validator.getClient(config, httpClient), }; } diff --git a/packages/api/src/beacon/client/lightclient.ts b/packages/api/src/beacon/client/lightclient.ts index 3f99eaf3b867..094ceb70f5a0 100644 --- a/packages/api/src/beacon/client/lightclient.ts +++ b/packages/api/src/beacon/client/lightclient.ts @@ -1,7 +1,6 @@ import {IChainForkConfig} from "@lodestar/config"; -import {deserializeProof} from "@chainsafe/persistent-merkle-tree"; import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/lightclient.js"; -import {IHttpClient, getFetchOptsSerializers, generateGenericJsonClient} from "../../utils/client/index.js"; +import {IHttpClient, generateGenericJsonClient} from "../../utils/client/index.js"; /** * REST HTTP client for lightclient routes @@ -9,19 +8,6 @@ import {IHttpClient, getFetchOptsSerializers, generateGenericJsonClient} from ". export function getClient(_config: IChainForkConfig, httpClient: IHttpClient): Api { const reqSerializers = getReqSerializers(); const returnTypes = getReturnTypes(); - - // Some routes return JSON, use a client auto-generator - const client = generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); - // For `getStateProof()` generate request serializer - const fetchOptsSerializers = getFetchOptsSerializers(routesData, reqSerializers); - - return { - ...client, - - async getStateProof(stateId, paths) { - const buffer = await httpClient.arrayBuffer(fetchOptsSerializers.getStateProof(stateId, paths)); - const proof = deserializeProof(new Uint8Array(buffer)); - return {data: proof}; - }, - }; + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); } diff --git a/packages/api/src/beacon/client/proof.ts b/packages/api/src/beacon/client/proof.ts new file mode 100644 index 000000000000..ce8bf1554fd1 --- /dev/null +++ b/packages/api/src/beacon/client/proof.ts @@ -0,0 +1,22 @@ +import {IChainForkConfig} from "@lodestar/config"; +import {deserializeProof} from "@chainsafe/persistent-merkle-tree"; +import {Api, ReqTypes, routesData, getReqSerializers} from "../routes/proof.js"; +import {IHttpClient, getFetchOptsSerializers} from "../../utils/client/index.js"; + +/** + * REST HTTP client for lightclient routes + */ +export function getClient(_config: IChainForkConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(); + + // For `getStateProof()` generate request serializer + const fetchOptsSerializers = getFetchOptsSerializers(routesData, reqSerializers); + + return { + async getStateProof(stateId, paths) { + const buffer = await httpClient.arrayBuffer(fetchOptsSerializers.getStateProof(stateId, paths)); + const proof = deserializeProof(new Uint8Array(buffer)); + return {data: proof}; + }, + }; +} diff --git a/packages/api/src/beacon/index.ts b/packages/api/src/beacon/index.ts index d94f09edcd23..e86895e3beae 100644 --- a/packages/api/src/beacon/index.ts +++ b/packages/api/src/beacon/index.ts @@ -16,6 +16,7 @@ const allNamespacesObj: {[K in keyof Api]: true} = { lightclient: true, lodestar: true, node: true, + proof: true, validator: true, }; export const allNamespaces = Object.keys(allNamespacesObj) as ApiNamespace[]; diff --git a/packages/api/src/beacon/routes/index.ts b/packages/api/src/beacon/routes/index.ts index da320080042b..f6d2b38b3457 100644 --- a/packages/api/src/beacon/routes/index.ts +++ b/packages/api/src/beacon/routes/index.ts @@ -5,6 +5,7 @@ import {Api as EventsApi} from "./events.js"; import {Api as LightclientApi} from "./lightclient.js"; import {Api as LodestarApi} from "./lodestar.js"; import {Api as NodeApi} from "./node.js"; +import {Api as ProofApi} from "./proof.js"; import {Api as ValidatorApi} from "./validator.js"; export * as beacon from "./beacon/index.js"; @@ -14,6 +15,7 @@ export * as events from "./events.js"; export * as lightclient from "./lightclient.js"; export * as lodestar from "./lodestar.js"; export * as node from "./node.js"; +export * as proof from "./proof.js"; export * as validator from "./validator.js"; export type Api = { @@ -24,6 +26,7 @@ export type Api = { lightclient: LightclientApi; lodestar: LodestarApi; node: NodeApi; + proof: ProofApi; validator: ValidatorApi; }; diff --git a/packages/api/src/beacon/routes/lightclient.ts b/packages/api/src/beacon/routes/lightclient.ts index 6c0fa1702c12..01f34b342ebc 100644 --- a/packages/api/src/beacon/routes/lightclient.ts +++ b/packages/api/src/beacon/routes/lightclient.ts @@ -1,18 +1,14 @@ -import {JsonPath} from "@chainsafe/ssz"; -import {Proof} from "@chainsafe/persistent-merkle-tree"; import {altair, phase0, ssz, SyncPeriod} from "@lodestar/types"; import { ArrayOf, ReturnTypes, RoutesData, Schema, - sameType, ContainerData, ReqSerializers, reqEmpty, ReqEmpty, } from "../../utils/index.js"; -import {queryParseProofPathsArr, querySerializeProofPathsArr} from "../../utils/serdes.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -24,11 +20,6 @@ export type LightClientBootstrap = { }; export type Api = { - /** - * Returns a multiproof of `jsonPaths` at the requested `stateId`. - * The requested `stateId` may not be available. Regular nodes only keep recent states in memory. - */ - getStateProof(stateId: string, jsonPaths: JsonPath[]): Promise<{data: Proof}>; /** * Returns an array of best updates given a `startPeriod` and `count` number of sync committee period to return. * Best is defined by (in order of priority): @@ -59,7 +50,6 @@ export type Api = { * Define javascript values for each route */ export const routesData: RoutesData = { - getStateProof: {url: "/eth/v1/beacon/light_client/proof/{state_id}", method: "GET"}, getUpdates: {url: "/eth/v1/beacon/light_client/updates", method: "GET"}, getOptimisticUpdate: {url: "/eth/v1/beacon/light_client/optimistic_update", method: "GET"}, getFinalityUpdate: {url: "/eth/v1/beacon/light_client/finality_update", method: "GET"}, @@ -69,7 +59,6 @@ export const routesData: RoutesData = { /* eslint-disable @typescript-eslint/naming-convention */ export type ReqTypes = { - getStateProof: {params: {state_id: string}; query: {paths: string[]}}; getUpdates: {query: {start_period: number; count: number}}; getOptimisticUpdate: ReqEmpty; getFinalityUpdate: ReqEmpty; @@ -79,12 +68,6 @@ export type ReqTypes = { export function getReqSerializers(): ReqSerializers { return { - getStateProof: { - writeReq: (state_id, paths) => ({params: {state_id}, query: {paths: querySerializeProofPathsArr(paths)}}), - parseReq: ({params, query}) => [params.state_id, queryParseProofPathsArr(query.paths)], - schema: {params: {state_id: Schema.StringRequired}, body: Schema.AnyArray}, - }, - getUpdates: { writeReq: (start_period, count) => ({query: {start_period, count}}), parseReq: ({query}) => [query.start_period, query.count], @@ -109,8 +92,6 @@ export function getReqSerializers(): ReqSerializers { export function getReturnTypes(): ReturnTypes { return { - // Just sent the proof JSON as-is - getStateProof: sameType(), getUpdates: ContainerData(ArrayOf(ssz.altair.LightClientUpdate)), getOptimisticUpdate: ContainerData(ssz.altair.LightClientOptimisticUpdate), getFinalityUpdate: ContainerData(ssz.altair.LightClientFinalityUpdate), diff --git a/packages/api/src/beacon/routes/proof.ts b/packages/api/src/beacon/routes/proof.ts new file mode 100644 index 000000000000..19f2caa56f3f --- /dev/null +++ b/packages/api/src/beacon/routes/proof.ts @@ -0,0 +1,43 @@ +import {JsonPath} from "@chainsafe/ssz"; +import {Proof} from "@chainsafe/persistent-merkle-tree"; +import {ReturnTypes, RoutesData, Schema, sameType, ReqSerializers} from "../../utils/index.js"; +import {queryParseProofPathsArr, querySerializeProofPathsArr} from "../../utils/serdes.js"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type Api = { + /** + * Returns a multiproof of `jsonPaths` at the requested `stateId`. + * The requested `stateId` may not be available. Regular nodes only keep recent states in memory. + */ + getStateProof(stateId: string, jsonPaths: JsonPath[]): Promise<{data: Proof}>; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getStateProof: {url: "/eth/v0/beacon/proof/state/{state_id}", method: "GET"}, +}; + +/* eslint-disable @typescript-eslint/naming-convention */ +export type ReqTypes = { + getStateProof: {params: {state_id: string}; query: {paths: string[]}}; +}; + +export function getReqSerializers(): ReqSerializers { + return { + getStateProof: { + writeReq: (state_id, paths) => ({params: {state_id}, query: {paths: querySerializeProofPathsArr(paths)}}), + parseReq: ({params, query}) => [params.state_id, queryParseProofPathsArr(query.paths)], + schema: {params: {state_id: Schema.StringRequired}, body: Schema.AnyArray}, + }, + }; +} + +export function getReturnTypes(): ReturnTypes { + return { + // Just sent the proof JSON as-is + getStateProof: sameType(), + }; +} diff --git a/packages/api/src/beacon/server/index.ts b/packages/api/src/beacon/server/index.ts index c768d17b2515..f56275403f0f 100644 --- a/packages/api/src/beacon/server/index.ts +++ b/packages/api/src/beacon/server/index.ts @@ -9,6 +9,7 @@ import * as events from "./events.js"; import * as lightclient from "./lightclient.js"; import * as lodestar from "./lodestar.js"; import * as node from "./node.js"; +import * as proof from "./proof.js"; import * as validator from "./validator.js"; // Re-export for convenience @@ -36,6 +37,7 @@ export function registerRoutes( lightclient: () => lightclient.getRoutes(config, api.lightclient), lodestar: () => lodestar.getRoutes(config, api.lodestar), node: () => node.getRoutes(config, api.node), + proof: () => proof.getRoutes(config, api.proof), validator: () => validator.getRoutes(config, api.validator), }; diff --git a/packages/api/src/beacon/server/lightclient.ts b/packages/api/src/beacon/server/lightclient.ts index 92b4a7bc2798..187562289226 100644 --- a/packages/api/src/beacon/server/lightclient.ts +++ b/packages/api/src/beacon/server/lightclient.ts @@ -1,28 +1,8 @@ import {IChainForkConfig} from "@lodestar/config"; -import {serializeProof} from "@chainsafe/persistent-merkle-tree"; import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/lightclient.js"; import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; export function getRoutes(config: IChainForkConfig, api: Api): ServerRoutes { - const reqSerializers = getReqSerializers(); - const serverRoutes = getGenericJsonServer( - {routesData, getReturnTypes, getReqSerializers}, - config, - api - ); - - return { - ...serverRoutes, - - // Non-JSON routes. Return binary - getStateProof: { - ...serverRoutes.getStateProof, - handler: async (req) => { - const args = reqSerializers.getStateProof.parseReq(req); - const {data: proof} = await api.getStateProof(...args); - // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer - return Buffer.from(serializeProof(proof)); - }, - }, - }; + // All routes return JSON, use a server auto-generator + return getGenericJsonServer({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/beacon/server/proof.ts b/packages/api/src/beacon/server/proof.ts new file mode 100644 index 000000000000..d7192b4f6e6d --- /dev/null +++ b/packages/api/src/beacon/server/proof.ts @@ -0,0 +1,26 @@ +import {IChainForkConfig} from "@lodestar/config"; +import {serializeProof} from "@chainsafe/persistent-merkle-tree"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/proof.js"; +import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; + +export function getRoutes(config: IChainForkConfig, api: Api): ServerRoutes { + const reqSerializers = getReqSerializers(); + const serverRoutes = getGenericJsonServer( + {routesData, getReturnTypes, getReqSerializers}, + config, + api + ); + + return { + // Non-JSON routes. Return binary + getStateProof: { + ...serverRoutes.getStateProof, + handler: async (req) => { + const args = reqSerializers.getStateProof.parseReq(req); + const {data: proof} = await api.getStateProof(...args); + // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer + return Buffer.from(serializeProof(proof)); + }, + }, + }; +} diff --git a/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts b/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts new file mode 100644 index 000000000000..5b3a8dea5b91 --- /dev/null +++ b/packages/api/test/unit/beacon/genericServerTest/proofs.test.ts @@ -0,0 +1,10 @@ +import {config} from "@lodestar/config/default"; +import {Api, ReqTypes} from "../../../../src/beacon/routes/proof.js"; +import {getClient} from "../../../../src/beacon/client/proof.js"; +import {getRoutes} from "../../../../src/beacon/server/proof.js"; +import {runGenericServerTest} from "../../../utils/genericServerTest.js"; +import {testData} from "../testData/proofs.js"; + +describe("beacon / proofs", () => { + runGenericServerTest(config, getClient, getRoutes, testData); +}); diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index 7721725b7a00..09f3cd7552df 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -15,6 +15,7 @@ import {testData as debugTestData} from "./testData/debug.js"; import {eventTestData, testData as eventsTestData} from "./testData/events.js"; import {testData as lightclientTestData} from "./testData/lightclient.js"; import {testData as nodeTestData} from "./testData/node.js"; +import {testData as proofsTestData} from "./testData/proofs.js"; import {testData as validatorTestData} from "./testData/validator.js"; // Global variable __dirname no longer available in ES6 modules. @@ -36,6 +37,7 @@ const routesData = { ...routes.events.routesData, ...routes.lightclient.routesData, ...routes.node.routesData, + ...routes.proof.routesData, ...routes.validator.routesData, }; @@ -57,6 +59,7 @@ const reqSerializers = { ...getEventsReqSerializers(), ...routes.lightclient.getReqSerializers(), ...routes.node.getReqSerializers(), + ...routes.proof.getReqSerializers(), ...routes.validator.getReqSerializers(), }; @@ -66,6 +69,7 @@ const returnTypes = { ...routes.debug.getReturnTypes(), ...routes.lightclient.getReturnTypes(), ...routes.node.getReturnTypes(), + ...routes.proof.getReturnTypes(), ...routes.validator.getReturnTypes(), }; @@ -76,6 +80,7 @@ const testDatas = { ...eventsTestData, ...lightclientTestData, ...nodeTestData, + ...proofsTestData, ...validatorTestData, }; diff --git a/packages/api/test/unit/beacon/testData/lightclient.ts b/packages/api/test/unit/beacon/testData/lightclient.ts index 0bd3926b7a25..cb43ac38066d 100644 --- a/packages/api/test/unit/beacon/testData/lightclient.ts +++ b/packages/api/test/unit/beacon/testData/lightclient.ts @@ -1,5 +1,4 @@ import {ssz} from "@lodestar/types"; -import {ProofType} from "@chainsafe/persistent-merkle-tree"; import {toHexString} from "@chainsafe/ssz"; import {Api} from "../../../../src/beacon/routes/lightclient.js"; import {GenericServerTestCases} from "../../../utils/genericServerTest.js"; @@ -12,30 +11,6 @@ const header = ssz.phase0.BeaconBlockHeader.defaultValue(); const signatureSlot = ssz.Slot.defaultValue(); export const testData: GenericServerTestCases = { - getStateProof: { - args: [ - "head", - [ - // ["validator", 0, "balance"], - ["finalized_checkpoint", 0, "root", 12000], - ], - ], - res: { - data: { - type: ProofType.treeOffset, - offsets: [1, 2, 3], - leaves: [root, root, root, root], - }, - }, - /* eslint-disable quotes */ - query: { - paths: [ - // '["validator",0,"balance"]', - '["finalized_checkpoint",0,"root",12000]', - ], - }, - /* eslint-enable quotes */ - }, getUpdates: { args: [1, 2], res: {data: [lightClientUpdate]}, diff --git a/packages/api/test/unit/beacon/testData/proofs.ts b/packages/api/test/unit/beacon/testData/proofs.ts new file mode 100644 index 000000000000..174975156bd0 --- /dev/null +++ b/packages/api/test/unit/beacon/testData/proofs.ts @@ -0,0 +1,32 @@ +import {ProofType} from "@chainsafe/persistent-merkle-tree"; +import {Api} from "../../../../src/beacon/routes/proof.js"; +import {GenericServerTestCases} from "../../../utils/genericServerTest.js"; + +const root = Uint8Array.from(Buffer.alloc(32, 1)); + +export const testData: GenericServerTestCases = { + getStateProof: { + args: [ + "head", + [ + // ["validator", 0, "balance"], + ["finalized_checkpoint", 0, "root", 12000], + ], + ], + res: { + data: { + type: ProofType.treeOffset, + offsets: [1, 2, 3], + leaves: [root, root, root, root], + }, + }, + /* eslint-disable quotes */ + query: { + paths: [ + // '["validator",0,"balance"]', + '["finalized_checkpoint",0,"root",12000]', + ], + }, + /* eslint-enable quotes */ + }, +}; diff --git a/packages/beacon-node/src/api/impl/api.ts b/packages/beacon-node/src/api/impl/api.ts index ff5872c29cb0..e7e5332eddb5 100644 --- a/packages/beacon-node/src/api/impl/api.ts +++ b/packages/beacon-node/src/api/impl/api.ts @@ -8,6 +8,7 @@ import {getEventsApi} from "./events/index.js"; import {getLightclientApi} from "./lightclient/index.js"; import {getLodestarApi} from "./lodestar/index.js"; import {getNodeApi} from "./node/index.js"; +import {getProofApi} from "./proof/index.js"; import {getValidatorApi} from "./validator/index.js"; export function getApi(opts: IApiOptions, modules: ApiModules): Api { @@ -16,9 +17,10 @@ export function getApi(opts: IApiOptions, modules: ApiModules): Api { config: getConfigApi(modules), debug: getDebugApi(modules), events: getEventsApi(modules), - lightclient: getLightclientApi(opts, modules), + lightclient: getLightclientApi(modules), lodestar: getLodestarApi(modules), node: getNodeApi(opts, modules), + proof: getProofApi(opts, modules), validator: getValidatorApi(modules), }; } diff --git a/packages/beacon-node/src/api/impl/lightclient/index.ts b/packages/beacon-node/src/api/impl/lightclient/index.ts index e4f570b8fc03..0e5d0128cbff 100644 --- a/packages/beacon-node/src/api/impl/lightclient/index.ts +++ b/packages/beacon-node/src/api/impl/lightclient/index.ts @@ -1,48 +1,13 @@ import {routes} from "@lodestar/api"; import {fromHexString} from "@chainsafe/ssz"; -import {ProofType, Tree} from "@chainsafe/persistent-merkle-tree"; import {SyncPeriod} from "@lodestar/types"; import {MAX_REQUEST_LIGHT_CLIENT_UPDATES, MAX_REQUEST_LIGHT_CLIENT_COMMITTEE_HASHES} from "@lodestar/params"; import {ApiModules} from "../types.js"; -import {resolveStateId} from "../beacon/state/utils.js"; -import {IApiOptions} from "../../options.js"; // TODO: Import from lightclient/server package -export function getLightclientApi( - opts: IApiOptions, - {chain, config, db}: Pick -): routes.lightclient.Api { - // It's currently possible to request gigantic proofs (eg: a proof of the entire beacon state) - // We want some some sort of resistance against this DoS vector. - const maxGindicesInProof = opts.maxGindicesInProof ?? 512; - +export function getLightclientApi({chain}: Pick): routes.lightclient.Api { return { - async getStateProof(stateId, jsonPaths) { - const state = await resolveStateId(config, chain, db, stateId); - - // Commit any changes before computing the state root. In normal cases the state should have no changes here - state.commit(); - const stateNode = state.node; - const tree = new Tree(stateNode); - - const gindexes = state.type.tree_createProofGindexes(stateNode, jsonPaths); - // TODO: Is it necessary to de-duplicate? - // It's not a problem if we overcount gindexes - const gindicesSet = new Set(gindexes); - - if (gindicesSet.size > maxGindicesInProof) { - throw new Error("Requested proof is too large."); - } - - return { - data: tree.getProof({ - type: ProofType.treeOffset, - gindices: Array.from(gindicesSet), - }), - }; - }, - async getUpdates(startPeriod: SyncPeriod, count: number) { const maxAllowedCount = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, count); const periods = Array.from({length: maxAllowedCount}, (_ignored, i) => i + startPeriod); diff --git a/packages/beacon-node/src/api/impl/proof/index.ts b/packages/beacon-node/src/api/impl/proof/index.ts new file mode 100644 index 000000000000..af5ffefc0b48 --- /dev/null +++ b/packages/beacon-node/src/api/impl/proof/index.ts @@ -0,0 +1,41 @@ +import {routes} from "@lodestar/api"; +import {ProofType, Tree} from "@chainsafe/persistent-merkle-tree"; +import {ApiModules} from "../types.js"; +import {resolveStateId} from "../beacon/state/utils.js"; +import {IApiOptions} from "../../options.js"; + +export function getProofApi( + opts: IApiOptions, + {chain, config, db}: Pick +): routes.proof.Api { + // It's currently possible to request gigantic proofs (eg: a proof of the entire beacon state) + // We want some some sort of resistance against this DoS vector. + const maxGindicesInProof = opts.maxGindicesInProof ?? 512; + + return { + async getStateProof(stateId, jsonPaths) { + const state = await resolveStateId(config, chain, db, stateId); + + // Commit any changes before computing the state root. In normal cases the state should have no changes here + state.commit(); + const stateNode = state.node; + const tree = new Tree(stateNode); + + const gindexes = state.type.tree_createProofGindexes(stateNode, jsonPaths); + // TODO: Is it necessary to de-duplicate? + // It's not a problem if we overcount gindexes + const gindicesSet = new Set(gindexes); + + if (gindicesSet.size > maxGindicesInProof) { + throw new Error("Requested proof is too large."); + } + + return { + data: tree.getProof({ + type: ProofType.treeOffset, + gindices: Array.from(gindicesSet), + }), + }; + }, + }; +} diff --git a/packages/beacon-node/test/e2e/chain/lightclient.test.ts b/packages/beacon-node/test/e2e/chain/lightclient.test.ts index 3a695ce2111f..076a3896ebec 100644 --- a/packages/beacon-node/test/e2e/chain/lightclient.test.ts +++ b/packages/beacon-node/test/e2e/chain/lightclient.test.ts @@ -71,7 +71,7 @@ describe("chain / lightclient", function () { options: { sync: {isSingleNode: true}, network: {allowPublishToZeroPeers: true}, - api: {rest: {enabled: true, api: ["lightclient"], port: restPort, address: "localhost"}}, + api: {rest: {enabled: true, api: ["lightclient", "proof"], port: restPort, address: "localhost"}}, chain: {blsVerifyAllMainThread: true}, }, validatorCount: validatorCount * validatorClientCount, diff --git a/packages/light-client/src/index.ts b/packages/light-client/src/index.ts index ff76f11381be..0ab6491c635c 100644 --- a/packages/light-client/src/index.ts +++ b/packages/light-client/src/index.ts @@ -246,7 +246,7 @@ export class Lightclient { async getHeadStateProof(paths: JsonPath[]): Promise<{proof: TreeOffsetProof; header: phase0.BeaconBlockHeader}> { const header = this.head.header; const stateId = toHexString(header.stateRoot); - const res = await this.api.lightclient.getStateProof(stateId, paths); + const res = await this.api.proof.getStateProof(stateId, paths); return { proof: res.data as TreeOffsetProof, header, diff --git a/packages/light-client/test/mocks/LightclientServerApiMock.ts b/packages/light-client/test/mocks/LightclientServerApiMock.ts index f375fb8fb8c2..5f8fa1ce15dc 100644 --- a/packages/light-client/test/mocks/LightclientServerApiMock.ts +++ b/packages/light-client/test/mocks/LightclientServerApiMock.ts @@ -7,18 +7,21 @@ import {altair, RootHex, SyncPeriod} from "@lodestar/types"; import {notNullish} from "@lodestar/utils"; import {BeaconStateAltair} from "../utils/types.js"; -export class LightclientServerApiMock implements routes.lightclient.Api { +export class ProofServerApiMock implements routes.proof.Api { readonly states = new Map(); - readonly updates = new Map(); - readonly snapshots = new Map(); - latestHeadUpdate: altair.LightClientOptimisticUpdate | null = null; - finalized: altair.LightClientFinalityUpdate | null = null; async getStateProof(stateId: string, paths: JsonPath[]): Promise<{data: Proof}> { const state = this.states.get(stateId); if (!state) throw Error(`stateId ${stateId} not available`); return {data: state.createProof(paths)}; } +} + +export class LightclientServerApiMock implements routes.lightclient.Api { + readonly updates = new Map(); + readonly snapshots = new Map(); + latestHeadUpdate: altair.LightClientOptimisticUpdate | null = null; + finalized: altair.LightClientFinalityUpdate | null = null; async getUpdates(from: SyncPeriod, to: SyncPeriod): Promise<{data: altair.LightClientUpdate[]}> { const updates: altair.LightClientUpdate[] = []; diff --git a/packages/light-client/test/unit/sync.node.test.ts b/packages/light-client/test/unit/sync.node.test.ts index 089064e4719b..2731c13dd11b 100644 --- a/packages/light-client/test/unit/sync.node.test.ts +++ b/packages/light-client/test/unit/sync.node.test.ts @@ -8,7 +8,7 @@ import {chainConfig as chainConfigDef} from "@lodestar/config/default"; import {createIBeaconConfig, IChainConfig} from "@lodestar/config"; import {toHexString} from "@chainsafe/ssz"; import {Lightclient, LightclientEvent} from "../../src/index.js"; -import {LightclientServerApiMock} from "../mocks/LightclientServerApiMock.js"; +import {LightclientServerApiMock, ProofServerApiMock} from "../mocks/LightclientServerApiMock.js"; import {EventsServerApiMock} from "../mocks/EventsServerApiMock.js"; import { computeLightclientUpdate, @@ -58,11 +58,13 @@ describe("sync", () => { // Create server impl mock backed const lightclientServerApi = new LightclientServerApiMock(); const eventsServerApi = new EventsServerApiMock(); + const proofServerApi = new ProofServerApiMock(); // Start server const opts: ServerOpts = {host: "127.0.0.1", port: 15000}; await startServer(opts, config, ({ lightclient: lightclientServerApi, events: eventsServerApi, + proof: proofServerApi, } as Partial) as Api); // Populate initial snapshot @@ -130,7 +132,7 @@ describe("sync", () => { // Provide the state to the lightclient server impl. Only the last one to test proof fetching if (slot === targetSlot) { - lightclientServerApi.states.set(toHexString(stateRoot), (state as BeaconStateAllForks) as BeaconStateAltair); + proofServerApi.states.set(toHexString(stateRoot), (state as BeaconStateAllForks) as BeaconStateAltair); } // Emit a new head update with the custom state root diff --git a/packages/light-client/test/utils/server.ts b/packages/light-client/test/utils/server.ts index 24a53fb6f46f..3ff205b8c213 100644 --- a/packages/light-client/test/utils/server.ts +++ b/packages/light-client/test/utils/server.ts @@ -17,7 +17,7 @@ export async function startServer(opts: ServerOpts, config: IChainForkConfig, ap querystringParser: querystring.parse, }); - registerRoutes(server, config, api, ["lightclient", "events"]); + registerRoutes(server, config, api, ["lightclient", "proof", "events"]); void server.register(fastifyCors, {origin: "*"}); diff --git a/packages/validator/test/utils/apiStub.ts b/packages/validator/test/utils/apiStub.ts index 34113db44cec..55b4a370569c 100644 --- a/packages/validator/test/utils/apiStub.ts +++ b/packages/validator/test/utils/apiStub.ts @@ -18,6 +18,7 @@ export function getApiClientStub( lightclient: sandbox.stub(api.lightclient), lodestar: sandbox.stub(api.lodestar), node: sandbox.stub(api.node), + proof: sandbox.stub(api.proof), validator: sandbox.stub(api.validator), }; }