Skip to content

Commit

Permalink
Implement SSZ responses in the light client REST APIs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zah committed Jul 2, 2022
1 parent 6a3bd89 commit f6ac177
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 57 deletions.
33 changes: 11 additions & 22 deletions beacon_chain/rpc/rest_beacon_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 (
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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 (
Expand Down
36 changes: 12 additions & 24 deletions beacon_chain/rpc/rest_debug_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
40 changes: 33 additions & 7 deletions beacon_chain/rpc/rest_light_client_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand All @@ -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 =
Expand All @@ -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)

Expand All @@ -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)
37 changes: 37 additions & 0 deletions beacon_chain/rpc/rest_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 7 additions & 1 deletion beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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].} =
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions beacon_chain/spec/eth2_apis/rest_debug_calls.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit f6ac177

Please sign in to comment.