Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add debug_getHistoricalSummaries endpoint #7245

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion packages/api/src/beacon/routes/lodestar.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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,
Expand All @@ -11,6 +13,8 @@ import {
} from "../../utils/codecs.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {FilterGetPeers, NodePeer, PeerDirection, PeerState} from "./node.js";
import {ContainerType, ValueOf} from "@chainsafe/ssz";
import {StateArgs} from "./beacon/state.js";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

Expand Down Expand Up @@ -75,6 +79,15 @@ export type LodestarNodePeer = NodePeer & {

export type LodestarThreadType = "main" | "network" | "discv5";

type HistoricalSummariesList = ValueOf<typeof HistoricalSummariesResponseType>;
const HistoricalSummariesResponseType = new ContainerType(
{
HistoricalSummaries: ssz.capella.HistoricalSummaries,
proof: ArrayOf(ssz.Bytes8),
},
{jsonCase: "eth2"}
);

export type Endpoints = {
/** Trigger to write a heapdump to disk at `dirpath`. May take > 1min */
writeHeapdump: Endpoint<
Expand Down Expand Up @@ -247,6 +260,9 @@ export type Endpoints = {
{root: RootHex; slot: Slot}[],
EmptyMeta
>;

/** Returns historical summaries and proof for a given state ID */
getHistoricalSummaries: Endpoint<"GET", StateArgs, {params: {state_id: string}}, HistoricalSummariesList, EmptyMeta>;
};

export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
Expand Down Expand Up @@ -387,5 +403,20 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getHistoricalSummaries: {
url: "/eth/v0/lodestar/historical_summaries/{state_id}",
method: "GET",
req: {
writeReq: ({stateId}) => ({params: {state_id: stateId.toString()}}),
parseReq: ({params}) => ({stateId: params.state_id}),
schema: {
params: {state_id: Schema.StringRequired},
},
},
resp: {
data: HistoricalSummariesResponseType,
meta: EmptyMetaCodec,
},
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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("lodestar", () => {
describe("get HistoricalSummaries as json", () => {
const mockApi = getMockApi<Endpoints>(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<AnyEndpoint>);
}
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({
// biome-ignore lint/style/useNamingConvention: <explanation>
Historical_summaries: [],
proof: [],
});
});
});
});
2 changes: 1 addition & 1 deletion packages/beacon-node/src/api/impl/debug/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
34 changes: 32 additions & 2 deletions packages/beacon-node/src/api/impl/lodestar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ 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 {ForkName, ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
import {getLatestWeakSubjectivityCheckpointEpoch, loadState} from "@lodestar/state-transition";
import {ssz} from "@lodestar/types";
import {toHex, toRootHex} from "@lodestar/utils";
import {BeaconChain} from "../../../chain/index.js";
Expand All @@ -14,6 +14,9 @@ import {IBeaconDb} from "../../../db/interface.js";
import {GossipType} from "../../../network/index.js";
import {profileNodeJS, writeHeapSnapshot} from "../../../util/profile.js";
import {ApiModules} from "../types.js";
import {Tree} from "@chainsafe/persistent-merkle-tree";
import {getStateSlotFromBytes} from "../../../index.js";
import {getStateResponseWithRegen} from "../beacon/state/utils.js";

export function getLodestarApi({
chain,
Expand Down Expand Up @@ -187,6 +190,33 @@ export function getLodestarApi({
async dumpDbStateIndex() {
return {data: await db.stateArchive.dumpRootIndexEntries()};
},
async getHistoricalSummaries({stateId}, _context) {
const {state} = await getStateResponseWithRegen(chain, stateId);
let slot: number;
if (state instanceof Uint8Array) {
slot = getStateSlotFromBytes(state);
} else {
slot = state.slot;
}
if (config.getForkSeq(slot) < ForkSeq.capella) {
throw new Error("Historical summaries are not supported before Capella");
}
const fork = config.getForkName(slot) as Exclude<ForkName, "phase0" | "altair" | "bellatrix">;

const stateView =
state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state.clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state.clone();
(state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state.clone()) as BeaconStateCapella;

You can avoid the any below by casting this to a BeaconStateCapella here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the proof size for HistoricalSummaries changed in deneb (and will maybe again in Electra). Is it still safe to cast it to BeaconStateCapella here since the shape of the state is changing somewhat?


const gindex = ssz[fork].BeaconState.getPathInfo(["historicalSummaries"]);
const proof = new Tree(stateView.node).getSingleProof(gindex.gindex);

return {
data: {
// biome-ignore lint/suspicious/noExplicitAny: state is definitely Capella or later based on above check
HistoricalSummaries: (stateView as any).historicalSummaries.toValue(),
proof: proof,
},
};
},
};
}

Expand Down
6 changes: 5 additions & 1 deletion packages/types/src/capella/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"}
);
Expand Down
Loading