diff --git a/packages/api/src/beacon/routes/lodestar.ts b/packages/api/src/beacon/routes/lodestar.ts index 81191efd2696..de05b7f2bb44 100644 --- a/packages/api/src/beacon/routes/lodestar.ts +++ b/packages/api/src/beacon/routes/lodestar.ts @@ -1,8 +1,11 @@ +import {ContainerType, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; -import {Epoch, RootHex, Slot} from "@lodestar/types"; +import {Epoch, RootHex, Slot, ssz} from "@lodestar/types"; import { + ArrayOf, EmptyArgs, EmptyMeta, + EmptyMetaCodec, EmptyRequest, EmptyRequestCodec, EmptyResponseCodec, @@ -10,6 +13,7 @@ import { JsonOnlyResponseCodec, } from "../../utils/codecs.js"; import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js"; +import {StateArgs} from "./beacon/state.js"; import {FilterGetPeers, NodePeer, PeerDirection, PeerState} from "./node.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -75,6 +79,16 @@ export type LodestarNodePeer = NodePeer & { export type LodestarThreadType = "main" | "network" | "discv5"; +const HistoricalSummariesResponseType = new ContainerType( + { + historicalSummaries: ssz.capella.HistoricalSummaries, + proof: ArrayOf(ssz.Bytes8), + }, + {jsonCase: "eth2"} +); + +export type HistoricalSummariesResponse = ValueOf; + export type Endpoints = { /** Trigger to write a heapdump to disk at `dirpath`. May take > 1min */ writeHeapdump: Endpoint< @@ -214,6 +228,16 @@ export type Endpoints = { {count: number} >; + /** Returns historical summaries and proof for a given state ID */ + getHistoricalSummaries: Endpoint< + // ⏎ + "GET", + StateArgs, + {params: {state_id: string}}, + HistoricalSummariesResponse, + EmptyMeta + >; + /** Dump Discv5 Kad values */ discv5GetKadValues: Endpoint< // ⏎ @@ -365,6 +389,21 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions ({params: {state_id: stateId.toString()}}), + parseReq: ({params}) => ({stateId: params.state_id}), + schema: { + params: {state_id: Schema.StringRequired}, + }, + }, + resp: { + data: HistoricalSummariesResponseType, + meta: EmptyMetaCodec, + }, + }, discv5GetKadValues: { url: "/eth/v1/debug/discv5_kad_values", method: "GET", diff --git a/packages/api/test/unit/beacon/genericServerTest/lodestar.test.ts b/packages/api/test/unit/beacon/genericServerTest/lodestar.test.ts new file mode 100644 index 000000000000..b0f78f4bd62c --- /dev/null +++ b/packages/api/test/unit/beacon/genericServerTest/lodestar.test.ts @@ -0,0 +1,53 @@ +import {config} from "@lodestar/config/default"; +import {FastifyInstance} from "fastify"; +import {afterAll, beforeAll, describe, expect, it, vi} from "vitest"; +import {getClient} from "../../../../src/beacon/client/lodestar.js"; +import {Endpoints, getDefinitions} from "../../../../src/beacon/routes/lodestar.js"; +import {getRoutes} from "../../../../src/beacon/server/lodestar.js"; +import {HttpClient} from "../../../../src/utils/client/httpClient.js"; +import {AnyEndpoint} from "../../../../src/utils/codecs.js"; +import {FastifyRoute} from "../../../../src/utils/server/index.js"; +import {WireFormat} from "../../../../src/utils/wireFormat.js"; +import {getMockApi, getTestServer} from "../../../utils/utils.js"; + +describe("beacon / lodestar", () => { + describe("get HistoricalSummaries as json", () => { + const mockApi = getMockApi(getDefinitions(config)); + let baseUrl: string; + let server: FastifyInstance; + + beforeAll(async () => { + const res = getTestServer(); + server = res.server; + for (const route of Object.values(getRoutes(config, mockApi))) { + server.route(route as FastifyRoute); + } + baseUrl = await res.start(); + }); + + afterAll(async () => { + if (server !== undefined) await server.close(); + }); + + it("getHistoricalSummaries", async () => { + mockApi.getHistoricalSummaries.mockResolvedValue({ + data: { + historicalSummaries: [], + proof: [], + }, + }); + + const httpClient = new HttpClient({baseUrl}); + const client = getClient(config, httpClient); + + const res = await client.getHistoricalSummaries({stateId: "head"}, {responseWireFormat: WireFormat.json}); + + expect(res.ok).toBe(true); + expect(res.wireFormat()).toBe(WireFormat.json); + expect(res.json().data).toStrictEqual({ + historical_summaries: [], + proof: [], + }); + }); + }); +}); diff --git a/packages/beacon-node/src/api/impl/debug/index.ts b/packages/beacon-node/src/api/impl/debug/index.ts index e6c104272237..a004bd80e8f2 100644 --- a/packages/beacon-node/src/api/impl/debug/index.ts +++ b/packages/beacon-node/src/api/impl/debug/index.ts @@ -2,7 +2,7 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import {ExecutionStatus} from "@lodestar/fork-choice"; import {ZERO_HASH_HEX} from "@lodestar/params"; -import {BeaconState} from "@lodestar/types"; +import {BeaconState, ssz} from "@lodestar/types"; import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getStateSlotFromBytes} from "../../../util/multifork.js"; import {getStateResponseWithRegen} from "../beacon/state/utils.js"; diff --git a/packages/beacon-node/src/api/impl/lodestar/index.ts b/packages/beacon-node/src/api/impl/lodestar/index.ts index 10787194f5f5..aeef2e11a83e 100644 --- a/packages/beacon-node/src/api/impl/lodestar/index.ts +++ b/packages/beacon-node/src/api/impl/lodestar/index.ts @@ -1,11 +1,12 @@ import fs from "node:fs"; import path from "node:path"; +import {Tree} from "@chainsafe/persistent-merkle-tree"; import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import {ChainForkConfig} from "@lodestar/config"; import {Repository} from "@lodestar/db"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {getLatestWeakSubjectivityCheckpointEpoch} from "@lodestar/state-transition"; +import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {BeaconStateCapella, getLatestWeakSubjectivityCheckpointEpoch, loadState} from "@lodestar/state-transition"; import {ssz} from "@lodestar/types"; import {toHex, toRootHex} from "@lodestar/utils"; import {BeaconChain} from "../../../chain/index.js"; @@ -13,6 +14,7 @@ import {QueuedStateRegenerator, RegenRequest} from "../../../chain/regen/index.j import {IBeaconDb} from "../../../db/interface.js"; import {GossipType} from "../../../network/index.js"; import {profileNodeJS, writeHeapSnapshot} from "../../../util/profile.js"; +import {getStateResponseWithRegen} from "../beacon/state/utils.js"; import {ApiModules} from "../types.js"; export function getLodestarApi({ @@ -187,6 +189,29 @@ export function getLodestarApi({ async dumpDbStateIndex() { return {data: await db.stateArchive.dumpRootIndexEntries()}; }, + + async getHistoricalSummaries({stateId}) { + const {state} = await getStateResponseWithRegen(chain, stateId); + + const stateView = ( + state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state.clone() + ) as BeaconStateCapella; + + const fork = config.getForkName(stateView.slot); + if (ForkSeq[fork] < ForkSeq.capella) { + throw new Error("Historical summaries are not supported before Capella"); + } + + const {gindex} = ssz[fork].BeaconState.getPathInfo(["historicalSummaries"]); + const proof = new Tree(stateView.node).getSingleProof(gindex); + + return { + data: { + historicalSummaries: stateView.historicalSummaries.toValue(), + proof: proof, + }, + }; + }, }; } diff --git a/packages/types/src/capella/sszTypes.ts b/packages/types/src/capella/sszTypes.ts index 3110e59111d9..057bf97650fe 100644 --- a/packages/types/src/capella/sszTypes.ts +++ b/packages/types/src/capella/sszTypes.ts @@ -125,6 +125,10 @@ export const HistoricalSummary = new ContainerType( {typeName: "HistoricalSummary", jsonCase: "eth2"} ); +export const HistoricalSummaries = new ListCompositeType(HistoricalSummary, HISTORICAL_ROOTS_LIMIT, { + typeName: "HistoricalSummaries", +}); + // we don't reuse bellatrix.BeaconState fields since we need to replace some keys // and we cannot keep order doing that export const BeaconState = new ContainerType( @@ -168,7 +172,7 @@ export const BeaconState = new ContainerType( nextWithdrawalIndex: WithdrawalIndex, // [New in Capella] nextWithdrawalValidatorIndex: ValidatorIndex, // [New in Capella] // Deep history valid from Capella onwards - historicalSummaries: new ListCompositeType(HistoricalSummary, HISTORICAL_ROOTS_LIMIT), // [New in Capella] + historicalSummaries: HistoricalSummaries, // [New in Capella] }, {typeName: "BeaconState", jsonCase: "eth2"} );