From 9efd26c2e9d444e90e4776d6d3aecfa11e8033f0 Mon Sep 17 00:00:00 2001 From: henridf Date: Fri, 11 Aug 2023 20:08:54 -0700 Subject: [PATCH] Add support for POST /eth/v2/beacon/blocks (#5214) * Add support for POST /eth/v2/beacon/blocks * More descriptive errors * Address review feedback * Return 500 (not 400) for a missing implementation case --- beacon_chain/rpc/rest_beacon_api.nim | 64 +++++++++++- .../eth2_apis/eth2_rest_serialization.nim | 98 ++++++++++++++++++- .../spec/eth2_apis/rest_beacon_calls.nim | 39 ++++++++ 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/beacon_chain/rpc/rest_beacon_api.nim b/beacon_chain/rpc/rest_beacon_api.nim index 07d8e4733..72351cb4c 100644 --- a/beacon_chain/rpc/rest_beacon_api.nim +++ b/beacon_chain/rpc/rest_beacon_api.nim @@ -894,8 +894,68 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonMsgResponse(BlockValidationSuccess) - # TODO - # Add POST /eth/v2/beacon/blocks + # https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 + router.api(MethodPost, "/eth/v2/beacon/blocks") do ( + contentBody: Option[ContentBody]) -> RestApiResponse: + let res = + block: + if contentBody.isNone(): + return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) + if request.headers.getString("broadcast_validation") != "gossip": + # TODO (henridf): support 'consensus' and 'consensus_and_equivocation' + # broadcast_validation + return RestApiResponse.jsonError( + Http500, "gossip broadcast_validation only supported") + let + body = contentBody.get() + version = request.headers.getString("eth-consensus-version") + var + restBlock = decodeBodyJsonOrSsz(RestPublishedSignedBlockContents, + body, version).valueOr: + return RestApiResponse.jsonError(Http400, InvalidBlockObjectError, + $error) + forked = ForkedSignedBeaconBlock.init(restBlock) + + # TODO (henridf): handle broadcast_validation flag + if restBlock.kind != node.dag.cfg.consensusForkAtEpoch( + getForkedBlockField(forked, slot).epoch): + doAssert strictVerification notin node.dag.updateFlags + return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) + + case restBlock.kind + of ConsensusFork.Phase0: + var blck = restBlock.phase0Data + blck.root = hash_tree_root(blck.message) + await node.router.routeSignedBeaconBlock(blck, + Opt.none(SignedBlobSidecars)) + of ConsensusFork.Altair: + var blck = restBlock.altairData + blck.root = hash_tree_root(blck.message) + await node.router.routeSignedBeaconBlock(blck, + Opt.none(SignedBlobSidecars)) + of ConsensusFork.Bellatrix: + var blck = restBlock.bellatrixData + blck.root = hash_tree_root(blck.message) + await node.router.routeSignedBeaconBlock(blck, + Opt.none(SignedBlobSidecars)) + of ConsensusFork.Capella: + var blck = restBlock.capellaData + blck.root = hash_tree_root(blck.message) + await node.router.routeSignedBeaconBlock(blck, + Opt.none(SignedBlobSidecars)) + of ConsensusFork.Deneb: + var blck = restBlock.denebData.signed_block + blck.root = hash_tree_root(blck.message) + await node.router.routeSignedBeaconBlock( + blck, Opt.some(asSeq restBlock.denebData.signed_blob_sidecars)) + + if res.isErr(): + return RestApiResponse.jsonError( + Http503, BeaconNodeInSyncError, $res.error()) + if res.get().isNone(): + return RestApiResponse.jsonError(Http202, BlockValidationError) + + return RestApiResponse.jsonMsgResponse(BlockValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlindedBlock # https://github.com/ethereum/beacon-APIs/blob/v2.4.0/apis/beacon/blocks/blinded_blocks.yaml diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 8610088ed..cd5f8a78a 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -3027,7 +3027,7 @@ proc decodeBody*( t: typedesc[RestPublishedSignedBlockContents], body: ContentBody, version: string - ): Result[RestPublishedSignedBlockContents, cstring] = + ): Result[RestPublishedSignedBlockContents, string] = if body.contentType == ApplicationJsonMediaType: let data = try: @@ -3043,7 +3043,9 @@ proc decodeBody*( return err("Unexpected deserialization error") ok(data) elif body.contentType == OctetStreamMediaType: - let consensusFork = ? ConsensusFork.decodeString(version) + let consensusFork = + decodeEthConsensusVersion(version).valueOr: + return err("Invalid or Unsupported consensus version") case consensusFork of ConsensusFork.Phase0: let blck = @@ -3116,6 +3118,98 @@ proc decodeBody*[T](t: typedesc[T], return err("Unexpected deserialization error") ok(data) +proc decodeBodyJsonOrSsz*( + t: typedesc[RestPublishedSignedBlockContents], + body: ContentBody, + version: string + ): Result[RestPublishedSignedBlockContents, string] = + if body.contentType == OctetStreamMediaType: + decodeBody(RestPublishedSignedBlockContents, body, version) + elif body.contentType == ApplicationJsonMediaType: + let consensusFork = + decodeEthConsensusVersion(version).valueOr: + return err("Invalid or Unsupported consensus version") + case consensusFork + of ConsensusFork.Phase0: + let blck = + try: + RestJson.decode(body.data, phase0.SignedBeaconBlock, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg(""), + data = string.fromBytes(body.data) + return err("Unable to deserialize JSON for fork " & + version & ": " & exc.formatMsg("")) + except CatchableError as exc: + return err("Unexpected JSON deserialization error: " & exc.msg) + ok(RestPublishedSignedBlockContents( + kind: ConsensusFork.Phase0, phase0Data: blck)) + of ConsensusFork.Altair: + let blck = + try: + RestJson.decode(body.data, altair.SignedBeaconBlock, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg(""), + data = string.fromBytes(body.data) + return err("Unable to deserialize data") + except CatchableError: + return err("Unexpected deserialization error") + ok(RestPublishedSignedBlockContents( + kind: ConsensusFork.Altair, altairData: blck)) + of ConsensusFork.Bellatrix: + let blck = + try: + RestJson.decode(body.data, bellatrix.SignedBeaconBlock, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg(""), + data = string.fromBytes(body.data) + return err("Unable to deserialize data") + except CatchableError: + return err("Unexpected deserialization error") + ok(RestPublishedSignedBlockContents( + kind: ConsensusFork.Bellatrix, bellatrixData: blck)) + of ConsensusFork.Capella: + let blck = + try: + RestJson.decode(body.data, capella.SignedBeaconBlock, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg(""), + data = string.fromBytes(body.data) + return err("Unable to deserialize data") + except CatchableError: + return err("Unexpected deserialization error") + ok(RestPublishedSignedBlockContents( + kind: ConsensusFork.Capella, capellaData: blck)) + of ConsensusFork.Deneb: + let blckContents = + try: + RestJson.decode(body.data, DenebSignedBlockContents, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg(""), + data = string.fromBytes(body.data) + return err("Unable to deserialize data") + except CatchableError: + return err("Unexpected deserialization error") + ok(RestPublishedSignedBlockContents( + kind: ConsensusFork.Deneb, denebData: blckContents)) + else: + return err("Unsupported or invalid content media type") + + proc decodeBodyJsonOrSsz*[T](t: typedesc[T], body: ContentBody): Result[T, cstring] = if body.contentType == ApplicationJsonMediaType: diff --git a/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim b/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim index 895b6ae87..264bab3bb 100644 --- a/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim @@ -184,6 +184,45 @@ proc publishSszBlock*( extraHeaders = @[("eth-consensus-version", consensus)]) return resp +proc publishBlockV2Plain(body: phase0.SignedBeaconBlock): RestPlainResponse {. + rest, endpoint: "/eth/v2/beacon/blocks", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 + +proc publishBlockV2Plain(body: altair.SignedBeaconBlock): RestPlainResponse {. + rest, endpoint: "/eth/v2/beacon/blocks", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 + +proc publishBlockV2Plain(body: bellatrix.SignedBeaconBlock): RestPlainResponse {. + rest, endpoint: "/eth/v2/beacon/blocks", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 + +proc publishBlockV2Plain(body: capella.SignedBeaconBlock): RestPlainResponse {. + rest, endpoint: "/eth/v2/beacon/blocks", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 + +proc publishBlockV2Plain(body: DenebSignedBlockContents): RestPlainResponse {. + rest, endpoint: "/eth/v2/beacon/blocks", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 + +proc publishBlockV2*( + client: RestClientRef, + blck: phase0.SignedBeaconBlock | altair.SignedBeaconBlock | + bellatrix.SignedBeaconBlock | capella.SignedBeaconBlock | + deneb.SignedBeaconBlock + ): Future[RestPlainResponse] {.async} = + let + consensus = typeof(blck).toFork.toString() + resp = await client.publishBlockV2Plain( + blck, extraHeaders = @[ + ("eth-consensus-version", consensus), + ("broadcast_validation", "gossip")]) + return resp + proc publishBlindedBlock*(body: phase0.SignedBeaconBlock): RestPlainResponse {. rest, endpoint: "/eth/v1/beacon/blinded_blocks", meth: MethodPost.}