Skip to content

Add REST endpoint to retrieve historical_summaries #6675

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

Merged
merged 9 commits into from
May 6, 2025
2 changes: 2 additions & 0 deletions beacon_chain/rpc/rest_constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,5 @@ const
"Unable to load state for parent block, database corrupt?"
RewardOverflowError* =
"Reward value overflow"
HistoricalSummariesUnavailable* =
"Historical summaries unavailable"
48 changes: 47 additions & 1 deletion beacon_chain/rpc/rest_nimbus_api.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Copyright (c) 2018-2025 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
Expand Down Expand Up @@ -531,3 +531,49 @@ proc installNimbusApiHandlers*(router: var RestRouter, node: BeaconNode) =
delay: uint64(delay.nanoseconds)
)
RestApiResponse.jsonResponsePlain(response)

router.metricsApi2(
MethodGet,
"/nimbus/v1/debug/beacon/states/{state_id}/historical_summaries",
{RestServerMetricsType.Status, Response},
) do(state_id: StateIdent) -> RestApiResponse:
let
sid = state_id.valueOr:
return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error)
bslot = node.getBlockSlotId(sid).valueOr:
return RestApiResponse.jsonError(Http404, StateNotFoundError, $error)
contentType = preferredContentType(jsonMediaType, sszMediaType).valueOr:
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)

node.withStateForBlockSlotId(bslot):
return withState(state):
when consensusFork >= ConsensusFork.Capella:
const historicalSummariesFork = historicalSummariesForkAtConsensusFork(
consensusFork
)
.expect("HistoricalSummariesFork for Capella onwards")

let response = getHistoricalSummariesResponse(historicalSummariesFork)(
historical_summaries: forkyState.data.historical_summaries,
proof: forkyState.data
.build_proof(historicalSummariesFork.historical_summaries_gindex)
.expect("Valid gindex"),
slot: bslot.slot,
)

if contentType == jsonMediaType:
RestApiResponse.jsonResponseFinalizedWVersion(
response,
node.getStateOptimistic(state),
node.dag.isFinalized(bslot.bid),
consensusFork,
)
elif contentType == sszMediaType:
let headers = [("eth-consensus-version", consensusFork.toString())]
RestApiResponse.sszResponse(response, headers)
else:
RestApiResponse.jsonError(Http500, InvalidAcceptError)
else:
RestApiResponse.jsonError(Http404, HistoricalSummariesUnavailable)

RestApiResponse.jsonError(Http404, StateNotFoundError)
4 changes: 4 additions & 0 deletions beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ RestJson.useDefaultSerializationFor(
GetForkChoiceResponse,
GetForkScheduleResponse,
GetGenesisResponse,
GetHistoricalSummariesV1Response,
GetHistoricalSummariesV1ResponseElectra,
GetKeystoresResponse,
GetNextWithdrawalsResponse,
GetPoolAttesterSlashingsResponse,
Expand Down Expand Up @@ -404,6 +406,8 @@ type
DataOptimisticAndFinalizedObject |
GetBlockV2Response |
GetDistributedKeystoresResponse |
GetHistoricalSummariesV1Response |
GetHistoricalSummariesV1ResponseElectra |
GetKeystoresResponse |
GetRemoteKeystoresResponse |
GetStateForkResponse |
Expand Down
115 changes: 115 additions & 0 deletions beacon_chain/spec/eth2_apis/rest_nimbus_calls.nim
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,118 @@ proc getTimeOffset*(client: RestClientRef,
let msg = "Error response (" & $resp.status & ") [" & error.message & "]"
raise (ref RestResponseError)(
msg: msg, status: error.code, message: error.message)

func decodeSszResponse(
T: type ForkedHistoricalSummariesWithProof,
data: openArray[byte],
historicalSummariesFork: HistoricalSummariesFork,
cfg: RuntimeConfig,
): T {.raises: [RestDecodingError].} =
case historicalSummariesFork
of HistoricalSummariesFork.Electra:
let summaries =
try:
SSZ.decode(data, GetHistoricalSummariesV1ResponseElectra)
except SerializationError as exc:
raise newException(RestDecodingError, exc.msg)
ForkedHistoricalSummariesWithProof.init(summaries)
of HistoricalSummariesFork.Capella:
let summaries =
try:
SSZ.decode(data, GetHistoricalSummariesV1Response)
except SerializationError as exc:
raise newException(RestDecodingError, exc.msg)
ForkedHistoricalSummariesWithProof.init(summaries)

proc decodeJsonResponse(
T: type ForkedHistoricalSummariesWithProof,
data: openArray[byte],
historicalSummariesFork: HistoricalSummariesFork,
cfg: RuntimeConfig,
): T {.raises: [RestDecodingError].} =
case historicalSummariesFork
of HistoricalSummariesFork.Electra:
let summaries = decodeBytes(
GetHistoricalSummariesV1ResponseElectra, data, Opt.none(ContentTypeData)
).valueOr:
raise newException(RestDecodingError, $error)
ForkedHistoricalSummariesWithProof.init(summaries)
of HistoricalSummariesFork.Capella:
let summaries = decodeBytes(
GetHistoricalSummariesV1Response, data, Opt.none(ContentTypeData)
).valueOr:
raise newException(RestDecodingError, $error)
ForkedHistoricalSummariesWithProof.init(summaries)

proc decodeHttpResponse(
T: type ForkedHistoricalSummariesWithProof,
data: openArray[byte],
mediaType: MediaType,
consensusFork: ConsensusFork,
cfg: RuntimeConfig,
): T {.raises: [RestDecodingError].} =
let historicalSummariesFork = historicalSummariesForkAtConsensusFork(consensusFork).valueOr:
raiseRestDecodingBytesError(cstring("Unsupported fork: " & $consensusFork))

if mediaType == OctetStreamMediaType:
ForkedHistoricalSummariesWithProof.decodeSszResponse(data, historicalSummariesFork, cfg)
elif mediaType == ApplicationJsonMediaType:
ForkedHistoricalSummariesWithProof.decodeJsonResponse(data, historicalSummariesFork, cfg)
else:
raise newException(RestDecodingError, "Unsupported content-type")

proc getHistoricalSummariesV1Plain*(
state_id: StateIdent
): RestPlainResponse {.
rest,
endpoint: "/nimbus/v1/debug/beacon/states/{state_id}/historical_summaries",
accept: preferSSZ,
meth: MethodGet
.}

proc getHistoricalSummariesV1*(
client: RestClientRef, state_id: StateIdent, cfg: RuntimeConfig, restAccept = ""
): Future[Opt[ForkedHistoricalSummariesWithProof]] {.
async: (
raises: [
CancelledError, RestEncodingError, RestDnsResolveError, RestCommunicationError,
RestDecodingError, RestResponseError,
]
)
.} =
let resp =
if len(restAccept) > 0:
await client.getHistoricalSummariesV1Plain(state_id, restAcceptType = restAccept)
else:
await client.getHistoricalSummariesV1Plain(state_id)

return
case resp.status
of 200:
if resp.contentType.isNone() or isWildCard(resp.contentType.get().mediaType):
raise newException(RestDecodingError, "Missing or incorrect Content-Type")
else:
let
consensusFork = ConsensusFork.decodeString(
resp.headers.getString("eth-consensus-version")
).valueOr:
raiseRestDecodingBytesError(error)
mediaType = resp.contentType.value().mediaType

Opt.some(
ForkedHistoricalSummariesWithProof.decodeHttpResponse(
resp.data, mediaType, consensusFork, cfg
)
)
of 404:
Opt.none(ForkedHistoricalSummariesWithProof)
of 400, 500:
let error = decodeBytes(RestErrorMessage, resp.data, resp.contentType).valueOr:
let msg =
"Incorrect response error format (" & $resp.status & ") [" & $error & "]"
raise (ref RestResponseError)(msg: msg, status: resp.status)
let msg = "Error response (" & $resp.status & ") [" & error.message & "]"
raise
(ref RestResponseError)(msg: msg, status: error.code, message: error.message)
else:
raiseRestResponseError(resp)
92 changes: 91 additions & 1 deletion beacon_chain/spec/eth2_apis/rest_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import
std/[json, tables],
stew/base10, web3/primitives, httputils,
stew/base10, web3/primitives, httputils, stew/bitops2,
".."/[deposit_snapshots, forks]

export forks, tables, httputils
Expand Down Expand Up @@ -1075,3 +1075,93 @@ func toValidatorIndex*(value: RestValidatorIndex): Result[ValidatorIndex,
err(ValidatorIndexError.TooHighValue)
else:
doAssert(false, "ValidatorIndex type size is incorrect")

## Types and helpers for historical_summaries + proof endpoint
const
# gIndex for historical_summaries field (27th field in BeaconState)
HISTORICAL_SUMMARIES_GINDEX* = GeneralizedIndex(59) # 32 + 27 = 59
HISTORICAL_SUMMARIES_GINDEX_ELECTRA* = GeneralizedIndex(91) # 64 + 27 = 91

type
# Note: these could go in separate Capella/Electra spec files if they were
# part of the specification.
HistoricalSummariesProof* = array[log2trunc(HISTORICAL_SUMMARIES_GINDEX), Eth2Digest]
HistoricalSummariesProofElectra* =
array[log2trunc(HISTORICAL_SUMMARIES_GINDEX_ELECTRA), Eth2Digest]

# REST API types
GetHistoricalSummariesV1Response* = object
historical_summaries*: HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT]
proof*: HistoricalSummariesProof
slot*: Slot

GetHistoricalSummariesV1ResponseElectra* = object
historical_summaries*: HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT]
proof*: HistoricalSummariesProofElectra
slot*: Slot

ForkyGetHistoricalSummariesV1Response* =
GetHistoricalSummariesV1Response |
GetHistoricalSummariesV1ResponseElectra

HistoricalSummariesFork* {.pure.} = enum
Capella = 0,
Electra = 1

# REST client response type
ForkedHistoricalSummariesWithProof* = object
case kind*: HistoricalSummariesFork
of HistoricalSummariesFork.Capella: capellaData*: GetHistoricalSummariesV1Response
of HistoricalSummariesFork.Electra: electraData*: GetHistoricalSummariesV1ResponseElectra

template historical_summaries_gindex*(
kind: static HistoricalSummariesFork): GeneralizedIndex =
case kind
of HistoricalSummariesFork.Electra:
HISTORICAL_SUMMARIES_GINDEX_ELECTRA
of HistoricalSummariesFork.Capella:
HISTORICAL_SUMMARIES_GINDEX

template getHistoricalSummariesResponse*(
kind: static HistoricalSummariesFork): auto =
when kind >= HistoricalSummariesFork.Electra:
GetHistoricalSummariesV1ResponseElectra
elif kind >= HistoricalSummariesFork.Capella:
GetHistoricalSummariesV1Response

template init*(
T: type ForkedHistoricalSummariesWithProof,
historical_summaries: GetHistoricalSummariesV1Response,
): T =
ForkedHistoricalSummariesWithProof(
kind: HistoricalSummariesFork.Capella, capellaData: historical_summaries
)

template init*(
T: type ForkedHistoricalSummariesWithProof,
historical_summaries: GetHistoricalSummariesV1ResponseElectra,
): T =
ForkedHistoricalSummariesWithProof(
kind: HistoricalSummariesFork.Electra, electraData: historical_summaries
)

template withForkyHistoricalSummariesWithProof*(
x: ForkedHistoricalSummariesWithProof, body: untyped): untyped =
case x.kind
of HistoricalSummariesFork.Electra:
const historicalFork {.inject, used.} = HistoricalSummariesFork.Electra
template forkySummaries: untyped {.inject, used.} = x.electraData
body
of HistoricalSummariesFork.Capella:
const historicalFork {.inject, used.} = HistoricalSummariesFork.Capella
template forkySummaries: untyped {.inject, used.} = x.capellaData
body

func historicalSummariesForkAtConsensusFork*(consensusFork: ConsensusFork): Opt[HistoricalSummariesFork] =
static: doAssert HistoricalSummariesFork.high == HistoricalSummariesFork.Electra
if consensusFork >= ConsensusFork.Electra:
Opt.some HistoricalSummariesFork.Electra
elif consensusFork >= ConsensusFork.Capella:
Opt.some HistoricalSummariesFork.Capella
else:
Opt.none HistoricalSummariesFork
Loading