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

Implement SSZ responses in the light client REST APIs #3836

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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: 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:
etan-status marked this conversation as resolved.
Show resolved Hide resolved
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))
Copy link
Contributor

Choose a reason for hiding this comment

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

Updates may be from different fork digests in the future, i.e., each update may become a ForkedLightClientUpdate. The response should be future proofed for that, similar to the libp2p protocol which already did this.

See ethereum/beacon-APIs#181 (comment)

And, originally, point 3.3 from ethereum/consensus-specs#2802 (comment)


# 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] =
etan-status marked this conversation as resolved.
Show resolved Hide resolved
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