Skip to content

Commit d6d2f00

Browse files
authored
Add REST endpoint to retrieve historical_summaries (#6675)
Add REST endpoint to retrieve historical_summaries with a proof against the beacon state root: /nimbus/v1/debug/beacon/states/{state_id}/historical_summaries
1 parent e73d945 commit d6d2f00

File tree

5 files changed

+259
-2
lines changed

5 files changed

+259
-2
lines changed

beacon_chain/rpc/rest_constants.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,5 @@ const
279279
"Unable to load state for parent block, database corrupt?"
280280
RewardOverflowError* =
281281
"Reward value overflow"
282+
HistoricalSummariesUnavailable* =
283+
"Historical summaries unavailable"

beacon_chain/rpc/rest_nimbus_api.nim

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# beacon_chain
2-
# Copyright (c) 2018-2024 Status Research & Development GmbH
2+
# Copyright (c) 2018-2025 Status Research & Development GmbH
33
# Licensed and distributed under either of
44
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
55
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
@@ -531,3 +531,49 @@ proc installNimbusApiHandlers*(router: var RestRouter, node: BeaconNode) =
531531
delay: uint64(delay.nanoseconds)
532532
)
533533
RestApiResponse.jsonResponsePlain(response)
534+
535+
router.metricsApi2(
536+
MethodGet,
537+
"/nimbus/v1/debug/beacon/states/{state_id}/historical_summaries",
538+
{RestServerMetricsType.Status, Response},
539+
) do(state_id: StateIdent) -> RestApiResponse:
540+
let
541+
sid = state_id.valueOr:
542+
return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error)
543+
bslot = node.getBlockSlotId(sid).valueOr:
544+
return RestApiResponse.jsonError(Http404, StateNotFoundError, $error)
545+
contentType = preferredContentType(jsonMediaType, sszMediaType).valueOr:
546+
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)
547+
548+
node.withStateForBlockSlotId(bslot):
549+
return withState(state):
550+
when consensusFork >= ConsensusFork.Capella:
551+
const historicalSummariesFork = historicalSummariesForkAtConsensusFork(
552+
consensusFork
553+
)
554+
.expect("HistoricalSummariesFork for Capella onwards")
555+
556+
let response = getHistoricalSummariesResponse(historicalSummariesFork)(
557+
historical_summaries: forkyState.data.historical_summaries,
558+
proof: forkyState.data
559+
.build_proof(historicalSummariesFork.historical_summaries_gindex)
560+
.expect("Valid gindex"),
561+
slot: bslot.slot,
562+
)
563+
564+
if contentType == jsonMediaType:
565+
RestApiResponse.jsonResponseFinalizedWVersion(
566+
response,
567+
node.getStateOptimistic(state),
568+
node.dag.isFinalized(bslot.bid),
569+
consensusFork,
570+
)
571+
elif contentType == sszMediaType:
572+
let headers = [("eth-consensus-version", consensusFork.toString())]
573+
RestApiResponse.sszResponse(response, headers)
574+
else:
575+
RestApiResponse.jsonError(Http500, InvalidAcceptError)
576+
else:
577+
RestApiResponse.jsonError(Http404, HistoricalSummariesUnavailable)
578+
579+
RestApiResponse.jsonError(Http404, StateNotFoundError)

beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ RestJson.useDefaultSerializationFor(
8484
GetForkChoiceResponse,
8585
GetForkScheduleResponse,
8686
GetGenesisResponse,
87+
GetHistoricalSummariesV1Response,
88+
GetHistoricalSummariesV1ResponseElectra,
8789
GetKeystoresResponse,
8890
GetNextWithdrawalsResponse,
8991
GetPoolAttesterSlashingsResponse,
@@ -404,6 +406,8 @@ type
404406
DataOptimisticAndFinalizedObject |
405407
GetBlockV2Response |
406408
GetDistributedKeystoresResponse |
409+
GetHistoricalSummariesV1Response |
410+
GetHistoricalSummariesV1ResponseElectra |
407411
GetKeystoresResponse |
408412
GetRemoteKeystoresResponse |
409413
GetStateForkResponse |

beacon_chain/spec/eth2_apis/rest_nimbus_calls.nim

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,118 @@ proc getTimeOffset*(client: RestClientRef,
7676
let msg = "Error response (" & $resp.status & ") [" & error.message & "]"
7777
raise (ref RestResponseError)(
7878
msg: msg, status: error.code, message: error.message)
79+
80+
func decodeSszResponse(
81+
T: type ForkedHistoricalSummariesWithProof,
82+
data: openArray[byte],
83+
historicalSummariesFork: HistoricalSummariesFork,
84+
cfg: RuntimeConfig,
85+
): T {.raises: [RestDecodingError].} =
86+
case historicalSummariesFork
87+
of HistoricalSummariesFork.Electra:
88+
let summaries =
89+
try:
90+
SSZ.decode(data, GetHistoricalSummariesV1ResponseElectra)
91+
except SerializationError as exc:
92+
raise newException(RestDecodingError, exc.msg)
93+
ForkedHistoricalSummariesWithProof.init(summaries)
94+
of HistoricalSummariesFork.Capella:
95+
let summaries =
96+
try:
97+
SSZ.decode(data, GetHistoricalSummariesV1Response)
98+
except SerializationError as exc:
99+
raise newException(RestDecodingError, exc.msg)
100+
ForkedHistoricalSummariesWithProof.init(summaries)
101+
102+
proc decodeJsonResponse(
103+
T: type ForkedHistoricalSummariesWithProof,
104+
data: openArray[byte],
105+
historicalSummariesFork: HistoricalSummariesFork,
106+
cfg: RuntimeConfig,
107+
): T {.raises: [RestDecodingError].} =
108+
case historicalSummariesFork
109+
of HistoricalSummariesFork.Electra:
110+
let summaries = decodeBytes(
111+
GetHistoricalSummariesV1ResponseElectra, data, Opt.none(ContentTypeData)
112+
).valueOr:
113+
raise newException(RestDecodingError, $error)
114+
ForkedHistoricalSummariesWithProof.init(summaries)
115+
of HistoricalSummariesFork.Capella:
116+
let summaries = decodeBytes(
117+
GetHistoricalSummariesV1Response, data, Opt.none(ContentTypeData)
118+
).valueOr:
119+
raise newException(RestDecodingError, $error)
120+
ForkedHistoricalSummariesWithProof.init(summaries)
121+
122+
proc decodeHttpResponse(
123+
T: type ForkedHistoricalSummariesWithProof,
124+
data: openArray[byte],
125+
mediaType: MediaType,
126+
consensusFork: ConsensusFork,
127+
cfg: RuntimeConfig,
128+
): T {.raises: [RestDecodingError].} =
129+
let historicalSummariesFork = historicalSummariesForkAtConsensusFork(consensusFork).valueOr:
130+
raiseRestDecodingBytesError(cstring("Unsupported fork: " & $consensusFork))
131+
132+
if mediaType == OctetStreamMediaType:
133+
ForkedHistoricalSummariesWithProof.decodeSszResponse(data, historicalSummariesFork, cfg)
134+
elif mediaType == ApplicationJsonMediaType:
135+
ForkedHistoricalSummariesWithProof.decodeJsonResponse(data, historicalSummariesFork, cfg)
136+
else:
137+
raise newException(RestDecodingError, "Unsupported content-type")
138+
139+
proc getHistoricalSummariesV1Plain*(
140+
state_id: StateIdent
141+
): RestPlainResponse {.
142+
rest,
143+
endpoint: "/nimbus/v1/debug/beacon/states/{state_id}/historical_summaries",
144+
accept: preferSSZ,
145+
meth: MethodGet
146+
.}
147+
148+
proc getHistoricalSummariesV1*(
149+
client: RestClientRef, state_id: StateIdent, cfg: RuntimeConfig, restAccept = ""
150+
): Future[Opt[ForkedHistoricalSummariesWithProof]] {.
151+
async: (
152+
raises: [
153+
CancelledError, RestEncodingError, RestDnsResolveError, RestCommunicationError,
154+
RestDecodingError, RestResponseError,
155+
]
156+
)
157+
.} =
158+
let resp =
159+
if len(restAccept) > 0:
160+
await client.getHistoricalSummariesV1Plain(state_id, restAcceptType = restAccept)
161+
else:
162+
await client.getHistoricalSummariesV1Plain(state_id)
163+
164+
return
165+
case resp.status
166+
of 200:
167+
if resp.contentType.isNone() or isWildCard(resp.contentType.get().mediaType):
168+
raise newException(RestDecodingError, "Missing or incorrect Content-Type")
169+
else:
170+
let
171+
consensusFork = ConsensusFork.decodeString(
172+
resp.headers.getString("eth-consensus-version")
173+
).valueOr:
174+
raiseRestDecodingBytesError(error)
175+
mediaType = resp.contentType.value().mediaType
176+
177+
Opt.some(
178+
ForkedHistoricalSummariesWithProof.decodeHttpResponse(
179+
resp.data, mediaType, consensusFork, cfg
180+
)
181+
)
182+
of 404:
183+
Opt.none(ForkedHistoricalSummariesWithProof)
184+
of 400, 500:
185+
let error = decodeBytes(RestErrorMessage, resp.data, resp.contentType).valueOr:
186+
let msg =
187+
"Incorrect response error format (" & $resp.status & ") [" & $error & "]"
188+
raise (ref RestResponseError)(msg: msg, status: resp.status)
189+
let msg = "Error response (" & $resp.status & ") [" & error.message & "]"
190+
raise
191+
(ref RestResponseError)(msg: msg, status: error.code, message: error.message)
192+
else:
193+
raiseRestResponseError(resp)

beacon_chain/spec/eth2_apis/rest_types.nim

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import
1717
std/[json, tables],
18-
stew/base10, web3/primitives, httputils,
18+
stew/base10, web3/primitives, httputils, stew/bitops2,
1919
".."/[deposit_snapshots, forks]
2020

2121
export forks, tables, httputils
@@ -1075,3 +1075,93 @@ func toValidatorIndex*(value: RestValidatorIndex): Result[ValidatorIndex,
10751075
err(ValidatorIndexError.TooHighValue)
10761076
else:
10771077
doAssert(false, "ValidatorIndex type size is incorrect")
1078+
1079+
## Types and helpers for historical_summaries + proof endpoint
1080+
const
1081+
# gIndex for historical_summaries field (27th field in BeaconState)
1082+
HISTORICAL_SUMMARIES_GINDEX* = GeneralizedIndex(59) # 32 + 27 = 59
1083+
HISTORICAL_SUMMARIES_GINDEX_ELECTRA* = GeneralizedIndex(91) # 64 + 27 = 91
1084+
1085+
type
1086+
# Note: these could go in separate Capella/Electra spec files if they were
1087+
# part of the specification.
1088+
HistoricalSummariesProof* = array[log2trunc(HISTORICAL_SUMMARIES_GINDEX), Eth2Digest]
1089+
HistoricalSummariesProofElectra* =
1090+
array[log2trunc(HISTORICAL_SUMMARIES_GINDEX_ELECTRA), Eth2Digest]
1091+
1092+
# REST API types
1093+
GetHistoricalSummariesV1Response* = object
1094+
historical_summaries*: HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT]
1095+
proof*: HistoricalSummariesProof
1096+
slot*: Slot
1097+
1098+
GetHistoricalSummariesV1ResponseElectra* = object
1099+
historical_summaries*: HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT]
1100+
proof*: HistoricalSummariesProofElectra
1101+
slot*: Slot
1102+
1103+
ForkyGetHistoricalSummariesV1Response* =
1104+
GetHistoricalSummariesV1Response |
1105+
GetHistoricalSummariesV1ResponseElectra
1106+
1107+
HistoricalSummariesFork* {.pure.} = enum
1108+
Capella = 0,
1109+
Electra = 1
1110+
1111+
# REST client response type
1112+
ForkedHistoricalSummariesWithProof* = object
1113+
case kind*: HistoricalSummariesFork
1114+
of HistoricalSummariesFork.Capella: capellaData*: GetHistoricalSummariesV1Response
1115+
of HistoricalSummariesFork.Electra: electraData*: GetHistoricalSummariesV1ResponseElectra
1116+
1117+
template historical_summaries_gindex*(
1118+
kind: static HistoricalSummariesFork): GeneralizedIndex =
1119+
case kind
1120+
of HistoricalSummariesFork.Electra:
1121+
HISTORICAL_SUMMARIES_GINDEX_ELECTRA
1122+
of HistoricalSummariesFork.Capella:
1123+
HISTORICAL_SUMMARIES_GINDEX
1124+
1125+
template getHistoricalSummariesResponse*(
1126+
kind: static HistoricalSummariesFork): auto =
1127+
when kind >= HistoricalSummariesFork.Electra:
1128+
GetHistoricalSummariesV1ResponseElectra
1129+
elif kind >= HistoricalSummariesFork.Capella:
1130+
GetHistoricalSummariesV1Response
1131+
1132+
template init*(
1133+
T: type ForkedHistoricalSummariesWithProof,
1134+
historical_summaries: GetHistoricalSummariesV1Response,
1135+
): T =
1136+
ForkedHistoricalSummariesWithProof(
1137+
kind: HistoricalSummariesFork.Capella, capellaData: historical_summaries
1138+
)
1139+
1140+
template init*(
1141+
T: type ForkedHistoricalSummariesWithProof,
1142+
historical_summaries: GetHistoricalSummariesV1ResponseElectra,
1143+
): T =
1144+
ForkedHistoricalSummariesWithProof(
1145+
kind: HistoricalSummariesFork.Electra, electraData: historical_summaries
1146+
)
1147+
1148+
template withForkyHistoricalSummariesWithProof*(
1149+
x: ForkedHistoricalSummariesWithProof, body: untyped): untyped =
1150+
case x.kind
1151+
of HistoricalSummariesFork.Electra:
1152+
const historicalFork {.inject, used.} = HistoricalSummariesFork.Electra
1153+
template forkySummaries: untyped {.inject, used.} = x.electraData
1154+
body
1155+
of HistoricalSummariesFork.Capella:
1156+
const historicalFork {.inject, used.} = HistoricalSummariesFork.Capella
1157+
template forkySummaries: untyped {.inject, used.} = x.capellaData
1158+
body
1159+
1160+
func historicalSummariesForkAtConsensusFork*(consensusFork: ConsensusFork): Opt[HistoricalSummariesFork] =
1161+
static: doAssert HistoricalSummariesFork.high == HistoricalSummariesFork.Electra
1162+
if consensusFork >= ConsensusFork.Electra:
1163+
Opt.some HistoricalSummariesFork.Electra
1164+
elif consensusFork >= ConsensusFork.Capella:
1165+
Opt.some HistoricalSummariesFork.Capella
1166+
else:
1167+
Opt.none HistoricalSummariesFork

0 commit comments

Comments
 (0)