From 4b7bb4796fd7752203fc120a0e062736cbe03ed8 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Tue, 4 Oct 2022 13:38:09 +0200 Subject: [PATCH] update for latest LC REST proposal (#4213) Implements the latest proposal for providing LC data via REST, as of https://github.com/ethereum/beacon-APIs/pull/247 with a v0 suffix. Requests: - `/eth/v0/beacon/light_client/bootstrap/{block_root}` - `/eth/v0/beacon/light_client/updates?start_period={start_period}&count={count}` - `/eth/v0/beacon/light_client/finality_update` - `/eth/v0/beacon/light_client/optimistic_update` HTTP Server-Sent Events (SSE): - `light_client_finality_update_v0` - `light_client_optimistic_update_v0` --- beacon_chain/rpc/rest_light_client_api.nim | 101 +++++++++++++++--- .../eth2_apis/eth2_rest_serialization.nim | 69 ++++++++++++ beacon_chain/spec/helpers.nim | 10 ++ beacon_chain/sync/sync_protocol.nim | 8 +- 4 files changed, 171 insertions(+), 17 deletions(-) diff --git a/beacon_chain/rpc/rest_light_client_api.nim b/beacon_chain/rpc/rest_light_client_api.nim index f1470adce..b3334c6a6 100644 --- a/beacon_chain/rpc/rest_light_client_api.nim +++ b/beacon_chain/rpc/rest_light_client_api.nim @@ -22,6 +22,14 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = "/eth/v0/beacon/light_client/bootstrap/{block_root}") do ( block_root: Eth2Digest) -> RestApiResponse: doAssert node.dag.lcDataStore.serve + let contentType = + block: + let res = preferredContentType(jsonMediaType, + sszMediaType) + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() + let vroot = block: if block_root.isErr(): return RestApiResponse.jsonError(Http400, InvalidBlockRootValueError, @@ -29,17 +37,35 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = block_root.get() let bootstrap = node.dag.getLightClientBootstrap(vroot) - if bootstrap.isOk: - return RestApiResponse.jsonResponse(bootstrap) - else: + if bootstrap.isNone: return RestApiResponse.jsonError(Http404, LCBootstrapUnavailable) + let + contextEpoch = bootstrap.get.contextEpoch + contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch) + return + if contentType == sszMediaType: + let headers = [("eth-consensus-version", contextFork.toString())] + RestApiResponse.sszResponse(bootstrap.get, headers) + elif contentType == jsonMediaType: + RestApiResponse.jsonResponseWVersion(bootstrap.get, contextFork) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) + # https://github.com/ethereum/beacon-APIs/pull/181 router.api(MethodGet, "/eth/v0/beacon/light_client/updates") do ( start_period: Option[SyncCommitteePeriod], count: Option[uint64] ) -> RestApiResponse: doAssert node.dag.lcDataStore.serve + let contentType = + block: + let res = preferredContentType(jsonMediaType, + sszMediaType) + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() + let vstart = block: if start_period.isNone(): return RestApiResponse.jsonError(Http400, MissingStartPeriodValueError) @@ -58,7 +84,7 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = rcount.get() let headPeriod = node.dag.head.slot.sync_committee_period - # Limit number of updates in response + # Limit number of updates in response maxSupportedCount = if vstart > headPeriod: 0'u64 @@ -67,31 +93,80 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = numPeriods = min(vcount, maxSupportedCount) onePastPeriod = vstart + numPeriods - var updates = newSeqOfCap[LightClientUpdate](numPeriods) + var updates = newSeqOfCap[RestVersioned[LightClientUpdate]](numPeriods) for period in vstart.. RestApiResponse: doAssert node.dag.lcDataStore.serve + let contentType = + block: + let res = preferredContentType(jsonMediaType, + sszMediaType) + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() + let finality_update = node.dag.getLightClientFinalityUpdate() - if finality_update.isSome: - return RestApiResponse.jsonResponse(finality_update) - else: + if finality_update.isNone: return RestApiResponse.jsonError(Http404, LCFinUpdateUnavailable) + let + contextEpoch = finality_update.get.contextEpoch + contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch) + return + if contentType == sszMediaType: + let headers = [("eth-consensus-version", contextFork.toString())] + RestApiResponse.sszResponse(finality_update.get, headers) + elif contentType == jsonMediaType: + RestApiResponse.jsonResponseWVersion(finality_update.get, contextFork) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) + # https://github.com/ethereum/beacon-APIs/pull/181 router.api(MethodGet, "/eth/v0/beacon/light_client/optimistic_update") do ( ) -> RestApiResponse: doAssert node.dag.lcDataStore.serve + let contentType = + block: + let res = preferredContentType(jsonMediaType, + sszMediaType) + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() + let optimistic_update = node.dag.getLightClientOptimisticUpdate() - if optimistic_update.isSome: - return RestApiResponse.jsonResponse(optimistic_update) - else: + if optimistic_update.isNone: return RestApiResponse.jsonError(Http404, LCOptUpdateUnavailable) + + let + contextEpoch = optimistic_update.get.contextEpoch + contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch) + return + if contentType == sszMediaType: + let headers = [("eth-consensus-version", contextFork.toString())] + RestApiResponse.sszResponse(optimistic_update.get, headers) + elif contentType == jsonMediaType: + RestApiResponse.jsonResponseWVersion(optimistic_update.get, contextFork) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 51a02b46f..ea2e88fb5 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -279,6 +279,53 @@ proc jsonResponseWOpt*(t: typedesc[RestApiResponse], data: auto, default RestApiResponse.response(res, Http200, "application/json") +proc jsonResponseWVersion*(t: typedesc[RestApiResponse], data: auto, + version: BeaconStateFork): RestApiResponse = + let + headers = [("eth-consensus-version", version.toString())] + res = + block: + var default: seq[byte] + try: + var stream = memoryOutput() + var writer = JsonWriter[RestJson].init(stream) + writer.beginRecord() + writer.writeField("version", version.toString()) + writer.writeField("data", data) + writer.endRecord() + stream.getOutput(seq[byte]) + except SerializationError: + default + except IOError: + default + RestApiResponse.response(res, Http200, "application/json", headers = headers) + +type RestVersioned*[T] = object + data*: T + jsonVersion*: BeaconStateFork + sszContext*: ForkDigest + +proc jsonResponseVersionedList*[T](t: typedesc[RestApiResponse], + entries: openArray[RestVersioned[T]] + ): RestApiResponse = + let res = + block: + var default: seq[byte] + try: + var stream = memoryOutput() + var writer = JsonWriter[RestJson].init(stream) + for e in writer.stepwiseArrayCreation(entries): + writer.beginRecord() + writer.writeField("version", e.jsonVersion.toString()) + writer.writeField("data", e.data) + writer.endRecord() + stream.getOutput(seq[byte]) + except SerializationError: + default + except IOError: + default + RestApiResponse.response(res, Http200, "application/json") + proc jsonResponsePlain*(t: typedesc[RestApiResponse], data: auto): RestApiResponse = let res = @@ -415,6 +462,28 @@ proc jsonErrorList*(t: typedesc[RestApiResponse], default RestApiResponse.error(status, data, "application/json") +proc sszResponseVersionedList*[T](t: typedesc[RestApiResponse], + entries: openArray[RestVersioned[T]] + ): RestApiResponse = + let res = + block: + var default: seq[byte] + try: + var stream = memoryOutput() + for e in entries: + var cursor = stream.delayFixedSizeWrite(sizeof(uint64)) + let initPos = stream.pos + stream.write e.sszContext.data + var writer = SszWriter.init(stream) + writer.writeValue e.data + cursor.finalWrite (stream.pos - initPos).uint64.toBytesLE() + stream.getOutput(seq[byte]) + except SerializationError: + default + except IOError: + default + RestApiResponse.response(res, Http200, "application/octet-stream") + proc sszResponsePlain*(t: typedesc[RestApiResponse], res: seq[byte], headers: openArray[RestKeyValueTuple] = [] ): RestApiResponse = diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 4863a005e..7210a8b35 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -308,6 +308,16 @@ template is_better_update*[A, B: SomeLightClientUpdate]( new_update: A, old_update: B): bool = is_better_data(toMeta(new_update), toMeta(old_update)) +# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#getlightclientbootstrap +func contextEpoch*(bootstrap: altair.LightClientBootstrap): Epoch = + bootstrap.header.slot.epoch + +# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#lightclientupdatesbyrange +# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#getlightclientfinalityupdate +# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#getlightclientoptimisticupdate +func contextEpoch*(update: SomeLightClientUpdate): Epoch = + update.attested_header.slot.epoch + # https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/bellatrix/beacon-chain.md#is_merge_transition_complete func is_merge_transition_complete*(state: bellatrix.BeaconState): bool = const defaultExecutionPayloadHeader = default(ExecutionPayloadHeader) diff --git a/beacon_chain/sync/sync_protocol.nim b/beacon_chain/sync/sync_protocol.nim index 7fa055ea7..e215199fa 100644 --- a/beacon_chain/sync/sync_protocol.nim +++ b/beacon_chain/sync/sync_protocol.nim @@ -561,7 +561,7 @@ p2pProtocol BeaconSync(version = 1, let bootstrap = dag.getLightClientBootstrap(blockRoot) if bootstrap.isOk: let - contextEpoch = bootstrap.get.header.slot.epoch + contextEpoch = bootstrap.get.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data await response.send(bootstrap.get, contextBytes) else: @@ -605,7 +605,7 @@ p2pProtocol BeaconSync(version = 1, let update = dag.getLightClientUpdateForPeriod(period) if update.isSome: let - contextEpoch = update.get.attested_header.slot.epoch + contextEpoch = update.get.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data await response.write(update.get, contextBytes) inc found @@ -629,7 +629,7 @@ p2pProtocol BeaconSync(version = 1, let finality_update = dag.getLightClientFinalityUpdate() if finality_update.isSome: let - contextEpoch = finality_update.get.attested_header.slot.epoch + contextEpoch = finality_update.get.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data await response.send(finality_update.get, contextBytes) else: @@ -655,7 +655,7 @@ p2pProtocol BeaconSync(version = 1, let optimistic_update = dag.getLightClientOptimisticUpdate() if optimistic_update.isSome: let - contextEpoch = optimistic_update.get.attested_header.slot.epoch + contextEpoch = optimistic_update.get.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data await response.send(optimistic_update.get, contextBytes) else: