From 7ab1856c0442e4b98104f2d8428bf39d70508a8f Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Thu, 16 Sep 2021 16:32:32 +0300 Subject: [PATCH] SSZ encoded responses for REST API calls. (#2851) --- beacon_chain/rpc/rest_beacon_api.nim | 35 +++++- beacon_chain/rpc/rest_debug_api.nim | 65 +++++++--- beacon_chain/rpc/rest_utils.nim | 4 + .../eth2_apis/eth2_rest_serialization.nim | 116 ++++++++++++++++-- beacon_chain/spec/forks.nim | 18 +++ vendor/nim-chronos | 2 +- vendor/nim-presto | 2 +- 7 files changed, 208 insertions(+), 34 deletions(-) diff --git a/beacon_chain/rpc/rest_beacon_api.nim b/beacon_chain/rpc/rest_beacon_api.nim index 8c99453e4..13ea231ee 100644 --- a/beacon_chain/rpc/rest_beacon_api.nim +++ b/beacon_chain/rpc/rest_beacon_api.nim @@ -653,10 +653,23 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = if res.isErr(): return RestApiResponse.jsonError(Http404, BlockNotFoundError) res.get() + let contentType = + block: + let res = preferredContentType("application/octet-stream", + "application/json") + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() return case bdata.data.kind of BeaconBlockFork.Phase0: - RestApiResponse.jsonResponse(bdata.data.phase0Block) + case contentType + of "application/octet-stream": + RestApiResponse.sszResponse(bdata.data.phase0Block) + of "application/json": + RestApiResponse.jsonResponse(bdata.data.phase0Block) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) of BeaconBlockFork.Altair: RestApiResponse.jsonError(Http404, BlockNotFoundError) @@ -672,7 +685,25 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = if res.isErr(): return RestApiResponse.jsonError(Http404, BlockNotFoundError) res.get() - return RestApiResponse.jsonResponsePlain(bdata.data.asSigned()) + let contentType = + block: + let res = preferredContentType("application/octet-stream", + "application/json") + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() + return + case contentType + of "application/octet-stream": + case bdata.data.kind + of BeaconBlockFork.Phase0: + RestApiResponse.sszResponse(bdata.data.phase0Block) + of BeaconBlockFork.Altair: + RestApiResponse.sszResponse(bdata.data.altairBlock) + of "application/json": + RestApiResponse.jsonResponsePlain(bdata.data.asSigned()) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockRoot router.api(MethodGet, "/api/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 da0d263f0..ec12277d1 100644 --- a/beacon_chain/rpc/rest_debug_api.nim +++ b/beacon_chain/rpc/rest_debug_api.nim @@ -21,13 +21,27 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonError(Http404, StateNotFoundError, $bres.error()) bres.get() + let contentType = + block: + let res = preferredContentType("application/octet-stream", + "application/json") + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() node.withStateForBlockSlot(bslot): - case stateData.data.beaconStateFork - of BeaconStateFork.forkPhase0: - return RestApiResponse.jsonResponse(stateData.data.hbsPhase0.data) - of BeaconStateFork.forkAltair: - return RestApiResponse.jsonError(Http404, StateNotFoundError) - return RestApiResponse.jsonError(Http500, InternalServerError) + return + case stateData.data.beaconStateFork + of BeaconStateFork.forkPhase0: + case contentType + of "application/octet-stream": + RestApiResponse.sszResponse(stateData.data.hbsPhase0.data) + of "application/json": + RestApiResponse.jsonResponse(stateData.data.hbsPhase0.data) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) + of BeaconStateFork.forkAltair: + RestApiResponse.jsonError(Http404, StateNotFoundError) + return RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Debug/getStateV2 router.api(MethodGet, @@ -43,17 +57,28 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonError(Http404, StateNotFoundError, $bres.error()) bres.get() + let contentType = + block: + let res = preferredContentType("application/octet-stream", + "application/json") + if res.isErr(): + return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) + res.get() node.withStateForBlockSlot(bslot): - case stateData.data.beaconStateFork - of BeaconStateFork.forkPhase0: - return RestApiResponse.jsonResponse( - (version: "phase0", data: stateData.data.hbsPhase0.data) - ) - of BeaconStateFork.forkAltair: - return RestApiResponse.jsonResponse( - (version: "altair", data: stateData.data.hbsAltair.data) - ) - return RestApiResponse.jsonError(Http500, InternalServerError) + return + case contentType + of "application/json": + RestApiResponse.jsonResponsePlain( + ForkedBeaconState.init(stateData.data)) + of "application/octet-stream": + case stateData.data.beaconStateFork + of BeaconStateFork.forkPhase0: + RestApiResponse.sszResponse(stateData.data.hbsPhase0.data) + of BeaconStateFork.forkAltair: + RestApiResponse.sszResponse(stateData.data.hbsAltair.data) + else: + RestApiResponse.jsonError(Http500, InvalidAcceptError) + return RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Debug/getDebugChainHeads router.api(MethodGet, @@ -69,11 +94,11 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = ) router.redirect( MethodGet, - "/eth/v1/debug/beacon/heads", - "/api/eth/v1/debug/beacon/heads" + "/eth/v2/debug/beacon/states/{state_id}", + "/api/eth/v2/debug/beacon/states/{state_id}" ) router.redirect( MethodGet, - "/eth/v2/debug/beacon/heads", - "/api/eth/v2/debug/beacon/heads" + "/eth/v1/debug/beacon/heads", + "/api/eth/v1/debug/beacon/heads" ) diff --git a/beacon_chain/rpc/rest_utils.nim b/beacon_chain/rpc/rest_utils.nim index 6b25e1df1..e949985ca 100644 --- a/beacon_chain/rpc/rest_utils.nim +++ b/beacon_chain/rpc/rest_utils.nim @@ -135,6 +135,10 @@ const "Peer not found" InvalidLogLevelValueError* = "Invalid log level value error" + ContentNotAcceptableError* = + "Could not find out accepted content type" + InvalidAcceptError* = + "Incorrect accept response type" InternalServerError* = "Internal server error" NoImplementationError* = diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 8ae28ac00..8145fa95a 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -4,16 +4,14 @@ # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. -import - std/[typetraits], - stew/[results, base10, byteutils, endians2], - presto/common, - libp2p/peerid, - serialization, - json_serialization, json_serialization/std/[options, net], - nimcrypto/utils as ncrutils, - ".."/forks, ".."/datatypes/[phase0, altair, merge], - "."/rest_types +import std/typetraits +import stew/[results, base10, byteutils, endians2], presto/common, + libp2p/peerid, serialization, + json_serialization, json_serialization/std/[options, net], + nimcrypto/utils as ncrutils +import ".."/forks, ".."/datatypes/[phase0, altair, merge], + ".."/".."/ssz/ssz_serialization, + "."/rest_types export results, peerid, common, serialization, json_serialization, options, net, @@ -30,6 +28,10 @@ const # Size of `ValidatorSig` hexadecimal value (without 0x) RootHashSize = sizeof(Eth2Digest) * 2 # Size of `xxx_root` hexadecimal value (without 0x) + Phase0Version = + [byte('p'), byte('h'), byte('a'), byte('s'), byte('e'), byte('0')] + AltairVersion = + [byte('a'), byte('l'), byte('t'), byte('a'), byte('i'), byte('r')] type RestGenericError* = object @@ -262,6 +264,21 @@ proc jsonErrorList*(t: typedesc[RestApiResponse], default RestApiResponse.error(status, data, "application/json") +proc sszResponse*(t: typedesc[RestApiResponse], data: auto): RestApiResponse = + let res = + block: + var default: seq[byte] + try: + var stream = memoryOutput() + var writer = SszWriter.init(stream) + writer.writeValue(data) + stream.getOutput(seq[byte]) + except SerializationError: + default + except IOError: + default + RestApiResponse.response(res, Http200, "application/octet-stream") + template hexOriginal(data: openarray[byte]): string = "0x" & ncrutils.toHex(data, true) @@ -692,6 +709,85 @@ proc writeValue*(writer: var JsonWriter[RestJson], writer.writeField("data", value.altairBlock) writer.endRecord() +# ForkedBeaconState +proc readValue*(reader: var JsonReader[RestJson], + value: var ForkedBeaconState) {. + raises: [IOError, SerializationError, Defect].} = + var + version: Option[BeaconStateFork] + data: Option[JsonString] + + for fieldName in readObjectFields(reader): + case fieldName + of "version": + if version.isSome(): + reader.raiseUnexpectedField("Multiple version fields found", + "ForkedBeaconState") + let vres = reader.readValue(string) + case vres + of "phase0": + version = some(BeaconStateFork.forkPhase0) + of "altair": + version = some(BeaconStateFork.forkAltair) + else: + reader.raiseUnexpectedValue("Incorrect version field value") + of "data": + if data.isSome(): + reader.raiseUnexpectedField("Multiple data fields found", + "ForkedBeaconState") + data = some(reader.readValue(JsonString)) + else: + reader.raiseUnexpectedField(fieldName, "ForkedBeaconState") + + if version.isNone(): + reader.raiseUnexpectedValue("Field version is missing") + if data.isNone(): + reader.raiseUnexpectedValue("Field data is missing") + + case version.get(): + of BeaconStateFork.forkPhase0: + let res = + try: + some(RestJson.decode(string(data.get()), phase0.BeaconState, + requireAllFields = true)) + except SerializationError: + none[phase0.BeaconState]() + if res.isNone(): + reader.raiseUnexpectedValue("Incorrect phase0 beacon state format") + value = ForkedBeaconState.init(res.get()) + of BeaconStateFork.forkAltair: + let res = + try: + some(RestJson.decode(string(data.get()), altair.BeaconState, + requireAllFields = true)) + except SerializationError: + none[altair.BeaconState]() + if res.isNone(): + reader.raiseUnexpectedValue("Incorrect altair beacon state format") + value = ForkedBeaconState.init(res.get()) + +proc writeValue*(writer: var JsonWriter[RestJson], value: ForkedBeaconState) {. + raises: [IOError, Defect].} = + writer.beginRecord() + case value.beaconStateFork + of BeaconStateFork.forkPhase0: + writer.writeField("version", "phase0") + writer.writeField("data", value.bsPhase0) + of BeaconStateFork.forkAltair: + writer.writeField("version", "altair") + writer.writeField("data", value.bsAltair) + writer.endRecord() + +template toSszType*(v: BeaconBlockFork): auto = + case v + of BeaconBlockFork.Phase0: Phase0Version + of BeaconBlockFork.Altair: AltairVersion + +template toSszType*(v: BeaconStateFork): auto = + case v + of BeaconStateFork.forkPhase0: Phase0Version + of BeaconStateFork.forkAltair: AltairVersion + proc parseRoot(value: string): Result[Eth2Digest, cstring] = try: ok(Eth2Digest(data: hexToByteArray[32](value))) diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index 304661143..3425c6873 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -27,6 +27,11 @@ type of forkPhase0: hbsPhase0*: phase0.HashedBeaconState of forkAltair: hbsAltair*: altair.HashedBeaconState + ForkedBeaconState* = object + case beaconStateFork*: BeaconStateFork + of forkPhase0: bsPhase0*: phase0.BeaconState + of forkAltair: bsAltair*: altair.BeaconState + BeaconBlockFork* {.pure.} = enum Phase0 Altair @@ -69,6 +74,19 @@ template init*(T: type ForkedSignedBeaconBlock, blck: phase0.SignedBeaconBlock): template init*(T: type ForkedSignedBeaconBlock, blck: altair.SignedBeaconBlock): T = T(kind: BeaconBlockFork.Altair, altairBlock: blck) +template init*(T: type ForkedBeaconState, state: phase0.BeaconState): T = + T(beaconStateFork: BeaconStateFork.forkPhase0, bsPhase0: state) +template init*(T: type ForkedBeaconState, state: altair.BeaconState): T = + T(beaconStateFork: BeaconStateFork.forkAltair, bsAltair: state) +template init*(T: type ForkedBeaconState, state: ForkedHashedBeaconState): T = + case state.beaconStateFork + of BeaconStateFork.forkPhase0: + T(beaconStateFork: BeaconStateFork.forkPhase0, + bsPhase0: state.hbsPhase0.data) + of BeaconStateFork.forkAltair: + T(beaconStateFork: BeaconStateFork.forkAltair, + bsAltair: state.hbsAltair.data) + template init*(T: type ForkedSignedBeaconBlock, forked: ForkedBeaconBlock, blockRoot: Eth2Digest, signature: ValidatorSig): T = case forked.kind diff --git a/vendor/nim-chronos b/vendor/nim-chronos index 05c91418b..5034f0a5a 160000 --- a/vendor/nim-chronos +++ b/vendor/nim-chronos @@ -1 +1 @@ -Subproject commit 05c91418be18c1f0507d477182952c1e2f8cb11d +Subproject commit 5034f0a5a6772576c78d6ef45dacfda6a6bc60c4 diff --git a/vendor/nim-presto b/vendor/nim-presto index e96c6ded2..89d321bc1 160000 --- a/vendor/nim-presto +++ b/vendor/nim-presto @@ -1 +1 @@ -Subproject commit e96c6ded2a0f3f3985b638a64d225390e8259801 +Subproject commit 89d321bc160976978028ddbd13fd7d46c7192034