From f6ac1774011c2c9a05a6dc71c8086ac2a429456f Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Sat, 2 Jul 2022 16:23:53 +0300 Subject: [PATCH] Implement SSZ responses in the light client REST APIs Other changes: * More DRY encoding of the Nimbus content type preference. * Switch if/elif/else to exhaustive case statements to guard the code better from future changes. --- beacon_chain/rpc/rest_beacon_api.nim | 33 +++++---------- beacon_chain/rpc/rest_debug_api.nim | 36 ++++++----------- beacon_chain/rpc/rest_light_client_api.nim | 40 +++++++++++++++---- beacon_chain/rpc/rest_utils.nim | 37 +++++++++++++++++ .../eth2_apis/eth2_rest_serialization.nim | 8 +++- .../spec/eth2_apis/rest_debug_calls.nim | 5 ++- vendor/nim-stew | 2 +- 7 files changed, 104 insertions(+), 57 deletions(-) diff --git a/beacon_chain/rpc/rest_beacon_api.nim b/beacon_chain/rpc/rest_beacon_api.nim index 2d3d113cf9..8510af19f9 100644 --- a/beacon_chain/rpc/rest_beacon_api.nim +++ b/beacon_chain/rpc/rest_beacon_api.nim @@ -785,22 +785,18 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonError( Http404, BlockNotFoundError, "v1 API supports only phase 0 blocks") - let contentType = - block: - let res = preferredContentType(jsonMediaType, - sszMediaType) - if res.isErr(): - return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) - res.get() + let responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) return - if contentType == sszMediaType: + case responseType + of sszResponseType: var data: seq[byte] if not node.dag.getBlockSSZ(bid, data): return RestApiResponse.jsonError(Http404, BlockNotFoundError) RestApiResponse.response(data, Http200, $sszMediaType) - elif contentType == jsonMediaType: + of jsonResponseType: let bdata = node.dag.getForkedBlock(bid).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) @@ -811,8 +807,6 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = # issue.. RestApiResponse.jsonError( Http404, BlockNotFoundError, "v1 API supports only phase 0 blocks") - else: - RestApiResponse.jsonError(Http500, InvalidAcceptError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2 router.api(MethodGet, "/eth/v2/beacon/blocks/{block_id}") do ( @@ -824,15 +818,12 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = bid = node.getBlockId(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) - let contentType = - block: - let res = preferredContentType(jsonMediaType, - sszMediaType) - if res.isErr(): - return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) - res.get() + let responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + return - if contentType == sszMediaType: + case responseType + of sszResponseType: var data: seq[byte] if not node.dag.getBlockSSZ(bid, data): return RestApiResponse.jsonError(Http404, BlockNotFoundError) @@ -843,7 +834,7 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = RestApiResponse.response(data, Http200, $sszMediaType, headers = headers) - elif contentType == jsonMediaType: + of jsonResponseType: let bdata = node.dag.getForkedBlock(bid).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) @@ -856,8 +847,6 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = node.getBlockOptimistic(bdata), headers ) - else: - RestApiResponse.jsonError(Http500, InvalidAcceptError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockRoot router.api(MethodGet, "/eth/v1/beacon/blocks/{block_id}/root") do ( diff --git a/beacon_chain/rpc/rest_debug_api.nim b/beacon_chain/rpc/rest_debug_api.nim index d7175c3073..9527fe5872 100644 --- a/beacon_chain/rpc/rest_debug_api.nim +++ b/beacon_chain/rpc/rest_debug_api.nim @@ -30,23 +30,15 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonError(Http404, StateNotFoundError, $bres.error()) bres.get() - let contentType = - block: - let res = preferredContentType(jsonMediaType, - sszMediaType) - if res.isErr(): - return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) - res.get() + + let responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + node.withStateForBlockSlotId(bslot): return case state.kind of BeaconStateFork.Phase0: - if contentType == sszMediaType: - RestApiResponse.sszResponse(state.phase0Data.data, []) - elif contentType == jsonMediaType: - RestApiResponse.jsonResponse(state.phase0Data.data) - else: - RestApiResponse.jsonError(Http500, InvalidAcceptError) + responseType.okResponse state.phase0Data.data of BeaconStateFork.Altair, BeaconStateFork.Bellatrix: RestApiResponse.jsonError(Http404, StateNotFoundError) return RestApiResponse.jsonError(Http404, StateNotFoundError) @@ -65,26 +57,22 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonError(Http404, StateNotFoundError, $bres.error()) bres.get() - let contentType = - block: - let res = preferredContentType(jsonMediaType, - sszMediaType) - if res.isErr(): - return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) - res.get() + + let responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + node.withStateForBlockSlotId(bslot): return - if contentType == jsonMediaType: + case responseType + of jsonResponseType: RestApiResponse.jsonResponseState( state, node.getStateOptimistic(state) ) - elif contentType == sszMediaType: + of sszResponseType: let headers = [("eth-consensus-version", state.kind.toString())] withState(state): RestApiResponse.sszResponse(state.data, headers) - else: - RestApiResponse.jsonError(Http500, InvalidAcceptError) return RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Debug/getDebugChainHeads diff --git a/beacon_chain/rpc/rest_light_client_api.nim b/beacon_chain/rpc/rest_light_client_api.nim index 70673a9754..b8e1f02680 100644 --- a/beacon_chain/rpc/rest_light_client_api.nim +++ b/beacon_chain/rpc/rest_light_client_api.nim @@ -13,6 +13,11 @@ import ../beacon_node, logScope: topics = "rest_light_client" +const + # TODO: This needs to be specified in the spec + # https://github.com/ethereum/beacon-APIs/pull/181#issuecomment-1172877455 + MAX_CLIENT_UPDATES = 10000 + proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = # https://github.com/ethereum/beacon-APIs/pull/181 router.api(MethodGet, @@ -25,9 +30,13 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = $block_root.error()) block_root.get() - let bootstrap = node.dag.getLightClientBootstrap(vroot) + let + responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + bootstrap = node.dag.getLightClientBootstrap(vroot) + if bootstrap.isOk: - return RestApiResponse.jsonResponse(bootstrap) + return responseType.okResponse bootstrap.get else: return RestApiResponse.jsonError(Http404, LCBootstrapUnavailable) @@ -53,7 +62,10 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonError(Http400, InvalidCountError, $rcount.error()) rcount.get() + let + responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) headPeriod = node.dag.head.slot.sync_committee_period # Limit number of updates in response maxSupportedCount = @@ -69,16 +81,26 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = let update = node.dag.getLightClientUpdateForPeriod(period) if update.isSome: updates.add update.get - return RestApiResponse.jsonResponse(updates) + + return + case responseType + of jsonResponseType: + RestApiResponse.jsonResponse(updates) + of sszResponseType: + RestApiResponse.sszResponse(updates.asSszList(MAX_CLIENT_UPDATES)) # https://github.com/ethereum/beacon-APIs/pull/181 router.api(MethodGet, "/eth/v0/beacon/light_client/finality_update") do ( ) -> RestApiResponse: doAssert node.dag.lcDataStore.serve - let finality_update = node.dag.getLightClientFinalityUpdate() + let + responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + finality_update = node.dag.getLightClientFinalityUpdate() + if finality_update.isSome: - return RestApiResponse.jsonResponse(finality_update) + return responseType.okResponse finality_update.get else: return RestApiResponse.jsonError(Http404, LCFinUpdateUnavailable) @@ -87,8 +109,12 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = "/eth/v0/beacon/light_client/optimistic_update") do ( ) -> RestApiResponse: doAssert node.dag.lcDataStore.serve - let optimistic_update = node.dag.getLightClientOptimisticUpdate() + let + responseType = request.pickResponseType().valueOr: + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + optimistic_update = node.dag.getLightClientOptimisticUpdate() + if optimistic_update.isSome: - return RestApiResponse.jsonResponse(optimistic_update) + return responseType.okResponse optimistic_update.get else: return RestApiResponse.jsonError(Http404, LCOptUpdateUnavailable) diff --git a/beacon_chain/rpc/rest_utils.nim b/beacon_chain/rpc/rest_utils.nim index cac09c3109..9338aad042 100644 --- a/beacon_chain/rpc/rest_utils.nim +++ b/beacon_chain/rpc/rest_utils.nim @@ -310,3 +310,40 @@ const jsonMediaType* = MediaType.init("application/json") sszMediaType* = MediaType.init("application/octet-stream") textEventStreamMediaType* = MediaType.init("text/event-stream") + +type + ResponseContentType* = enum + # Please note that the order of content types here determines + # the order of preference for the returned result by Nimbus. + # This Nimbus preference is used when the request doesn't + # explicitly state another preference. + jsonResponseType = "application/json" + sszResponseType = "application/octet-stream" + +proc pickResponseType*(request: HttpRequestRef): Result[ResponseContentType, void] = + proc mediaTypesSeq: seq[MediaType] {.compileTime.} = + for t in enumStrValuesArray ResponseContentType: + result.add MediaType.init(t) + + const mediaTypes = mediaTypesSeq() + + let pick = try: request.preferredContentType(mediaTypes) + except ValueError: + # TODO: Fix this API in Chronos. + # Mixing Result with Exceptions is awkward to use. + return err() + if pick.isErr: + return err() + + for idx, mediaType in mediaTypes: + if pick.get == mediaType: + return ok ResponseContentType(idx) + +proc okResponse*( + responseType: ResponseContentType, + value: auto): RestApiResponse = + case responseType + of jsonResponseType: + RestApiResponse.jsonResponse value + of sszResponseType: + RestApiResponse.sszResponse value diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index fd0b948534..cd1c05b83e 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -131,6 +131,12 @@ type {.push raises: [Defect].} +template asSszList*[T](x: seq[T], limit: static Limit): untyped = + # XXX This works around generic instantiation problems + type E = typeof x[0] + const L = limit + List[E, L](x) + proc writeValue*(writer: var JsonWriter[RestJson], epochFlags: EpochParticipationFlags) {.raises: [IOError, Defect].} = @@ -448,7 +454,7 @@ proc jsonErrorList*(t: typedesc[RestApiResponse], RestApiResponse.error(status, data, "application/json") proc sszResponse*(t: typedesc[RestApiResponse], data: auto, - headers: openArray[tuple[key: string, value: string]] + headers: openArray[tuple[key: string, value: string]] = [] ): RestApiResponse = let res = block: diff --git a/beacon_chain/spec/eth2_apis/rest_debug_calls.nim b/beacon_chain/spec/eth2_apis/rest_debug_calls.nim index 4851db67e2..de5cb9046b 100644 --- a/beacon_chain/spec/eth2_apis/rest_debug_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_debug_calls.nim @@ -94,8 +94,9 @@ proc getStateV2*(client: RestClientRef, state_id: StateIdent, of "application/json": let state = block: - let res = newClone(decodeBytes(GetStateV2Response, resp.data, - resp.contentType)) + let res = newClone(decodeBytes(GetStateV2Response, + resp.data, + resp.contentType)) if res[].isErr(): raise newException(RestError, $res[].error()) newClone(res[].get()) diff --git a/vendor/nim-stew b/vendor/nim-stew index f75c0a273a..76e042d6ac 160000 --- a/vendor/nim-stew +++ b/vendor/nim-stew @@ -1 +1 @@ -Subproject commit f75c0a273aa34880ae7ac99e7e0c5d16b26ef166 +Subproject commit 76e042d6ac4115f5f0e0cd3adba77a9a54b9c658