Skip to content

Commit

Permalink
implement /eth/v1/beacon/blinded_blocks BN endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
tersec committed Nov 7, 2022
1 parent 8417b7e commit 55611d7
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 106 deletions.
72 changes: 72 additions & 0 deletions beacon_chain/rpc/rest_beacon_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import
../consensus_object_pools/[blockchain_dag, exit_pool, spec_cache],
../spec/[eth2_merkleization, forks, network, validator],
../spec/datatypes/[phase0, altair],
../validators/message_router_mev,
./state_ttl_cache

export rest_utils
Expand Down Expand Up @@ -795,6 +796,77 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =

return RestApiResponse.jsonMsgResponse(BlockValidationSuccess)

# https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlindedBlock
# https://github.com/ethereum/beacon-APIs/blob/v2.3.0/apis/beacon/blocks/blinded_blocks.yaml
router.api(MethodPost, "/eth/v1/beacon/blinded_blocks") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
## Instructs the beacon node to use the components of the
## `SignedBlindedBeaconBlock` to construct and publish a
## `SignedBeaconBlock` by swapping out the transactions_root for the
## corresponding full list of transactions. The beacon node should
## broadcast a newly constructed `SignedBeaconBlock` to the beacon network,
## to be included in the beacon chain. The beacon node is not required to
## validate the signed `BeaconBlock`, and a successful response (20X) only
## indicates that the broadcast has been successful.
if contentBody.isNone():
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)

let
currentEpochFork =
node.dag.cfg.stateForkAtEpoch(node.currentSlot().epoch())
version = request.headers.getString("eth-consensus-version")
body = contentBody.get()

if body.contentType == OctetStreamMediaType and
currentEpochFork.toString != version:
return RestApiResponse.jsonError(Http400, BlockIncorrectFork)

case currentEpochFork
of BeaconStateFork.Capella:
return RestApiResponse.jsonError(Http500, $capellaImplementationMissing)
of BeaconStateFork.Bellatrix:
let res =
block:
var restBlock = decodeBodyJsonOrSsz(SignedBlindedBeaconBlock, body).valueOr:
return RestApiResponse.jsonError(Http400, InvalidBlockObjectError,
$error)
await node.unblindAndRouteBlockMEV(restBlock)

if res.get().isErr():
return RestApiResponse.jsonError(
Http503, BeaconNodeInSyncError, $res.error())
if res.get().isNone():
return RestApiResponse.jsonError(Http202, BlockValidationError)

return RestApiResponse.jsonMsgResponse(BlockValidationSuccess)
of BeaconStateFork.Altair, BeaconStateFork.Phase0:
# Pre-Bellatrix, this endpoint will accept a `SignedBeaconBlock`.
#
# This is mostly the same as /eth/v1/beacon/blocks for phase 0 and
# altair.
var
restBlock = decodeBody(RestPublishedSignedBeaconBlock, body,
version).valueOr:
return RestApiResponse.jsonError(Http400, InvalidBlockObjectError,
$error)
forked = ForkedSignedBeaconBlock(restBlock)

if forked.kind != node.dag.cfg.blockForkAtEpoch(
getForkedBlockField(forked, slot).epoch):
return RestApiResponse.jsonError(Http400, InvalidBlockObjectError)

let res = withBlck(forked):
blck.root = hash_tree_root(blck.message)
await node.router.routeSignedBeaconBlock(blck)

if res.isErr():
return RestApiResponse.jsonError(
Http503, BeaconNodeInSyncError, $res.error())
elif res.get().isNone():
return RestApiResponse.jsonError(Http202, BlockValidationError)

return RestApiResponse.jsonMsgResponse(BlockValidationSuccess)

# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlock
router.api(MethodGet, "/eth/v1/beacon/blocks/{block_id}") do (
block_id: BlockIdent) -> RestApiResponse:
Expand Down
2 changes: 2 additions & 0 deletions beacon_chain/rpc/rest_constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,5 @@ const
DeprecatedRemovalValidatorBlocksV1* =
"v1/validator/blocks/{slot} endpoint was deprecated and replaced by v2, see " &
"https://github.com/ethereum/beacon-APIs/pull/220"
BlockIncorrectFork* =
"Block has incorrect fork"
28 changes: 28 additions & 0 deletions beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2446,6 +2446,34 @@ proc decodeBody*[T](t: typedesc[T],
return err("Unexpected deserialization error")
ok(data)

proc decodeBodyJsonOrSsz*[T](t: typedesc[T],
body: ContentBody): Result[T, cstring] =
if body.contentType == ApplicationJsonMediaType:
let data =
try:
RestJson.decode(body.data, T,
requireAllFields = true,
allowUnknownFields = true)
except SerializationError as exc:
debug "Failed to deserialize REST JSON data",
err = exc.formatMsg("<data>"),
data = string.fromBytes(body.data)
return err("Unable to deserialize data")
except CatchableError:
return err("Unexpected deserialization error")
ok(data)
elif body.contentType == OctetStreamMediaType:
let blck =
try:
SSZ.decode(body.data, T)
except SerializationError:
return err("Unable to deserialize data")
except CatchableError:
return err("Unexpected deserialization error")
ok(blck)
else:
return err("Unsupported content type")

proc encodeBytes*[T: EncodeTypes](value: T,
contentType: string): RestResult[seq[byte]] =
case contentType
Expand Down
122 changes: 122 additions & 0 deletions beacon_chain/validators/message_router_mev.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# beacon_chain
# Copyright (c) 2018-2022 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * 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.

when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}

import std/macros
import metrics
import ../beacon_node

from eth/async_utils import awaitWithTimeout
from ../spec/mev/rest_bellatrix_mev_calls import submitBlindedBlock

const
BUILDER_BLOCK_SUBMISSION_DELAY_TOLERANCE = 4.seconds

declareCounter beacon_block_builder_proposed,
"Number of beacon chain blocks produced using an external block builder"

func getFieldNames*(x: typedesc[auto]): seq[string] {.compileTime.} =
var res: seq[string]
for name, _ in fieldPairs(default(x)):
res.add name
res

macro copyFields*(
dst: untyped, src: untyped, fieldNames: static[seq[string]]): untyped =
result = newStmtList()
for name in fieldNames:
if name notin [
# These fields are the ones which vary between the blinded and
# unblinded objects, and can't simply be copied.
"transactions_root", "execution_payload",
"execution_payload_header", "body"]:
# TODO use stew/assign2
result.add newAssignment(
newDotExpr(dst, ident(name)), newDotExpr(src, ident(name)))

proc unblindAndRouteBlockMEV*(
node: BeaconNode, blindedBlock: SignedBlindedBeaconBlock):
Future[Result[Opt[BlockRef], string]] {.async.} =
# By time submitBlindedBlock is called, must already have done slashing
# protection check
let unblindedPayload =
try:
awaitWithTimeout(
node.payloadBuilderRestClient.submitBlindedBlock(blindedBlock),
BUILDER_BLOCK_SUBMISSION_DELAY_TOLERANCE):
error "Submitting blinded block timed out",
blk = shortLog(blindedBlock)
return err("Submitting blinded block timed out")
# From here on, including error paths, disallow local EL production by
# returning Opt.some, regardless of whether on head or newBlock.
except RestDecodingError as exc:
error "unblindAndRouteBlockMEV: REST decoding error submitting blinded block",
blindedBlock, error = exc.msg
return err("REST decoding error submitting blinded block: " & exc.msg)
except CatchableError as exc:
error "unblindAndRouteBlockMEV: exception in submitBlindedBlock",
blindedBlock, error = exc.msg
return err("exception in submitBlindedBlock: " & exc.msg)

const httpOk = 200
if unblindedPayload.status == httpOk:
if hash_tree_root(
blindedBlock.message.body.execution_payload_header) !=
hash_tree_root(unblindedPayload.data.data):
debug "unblindAndRouteBlockMEV: unblinded payload doesn't match blinded payload",
blindedPayload =
blindedBlock.message.body.execution_payload_header
else:
# Signature provided is consistent with unblinded execution payload,
# so construct full beacon block
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal
var signedBlock = bellatrix.SignedBeaconBlock(
signature: blindedBlock.signature)
copyFields(
signedBlock.message, blindedBlock.message,
getFieldNames(typeof(signedBlock.message)))
copyFields(
signedBlock.message.body, blindedBlock.message.body,
getFieldNames(typeof(signedBlock.message.body)))
signedBlock.message.body.execution_payload = unblindedPayload.data.data

if signedBlock.root != hash_tree_root(blindedBlock.message):
return err("Unblinded block doesn't match blinded block SSZ root")

signedBlock.root = hash_tree_root(signedBlock.message)

debug "unblindAndRouteBlockMEV: proposing unblinded block",
blck = shortLog(signedBlock)

let newBlockRef =
(await node.router.routeSignedBeaconBlock(signedBlock)).valueOr:
# submitBlindedBlock has run, so don't allow fallback to run
return err("routeSignedBeaconBlock error") # Errors logged in router

if newBlockRef.isSome:
beacon_block_builder_proposed.inc()
notice "Block proposed (MEV)",
blockRoot = shortLog(signedBlock.root), blck = shortLog(signedBlock),
signature = shortLog(signedBlock.signature)

return ok newBlockRef
else:
debug "unblindAndRouteBlockMEV: submitBlindedBlock failed",
blindedBlock, payloadStatus = unblindedPayload.status

# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#proposer-slashing
# This means if a validator publishes a signature for a
# `BlindedBeaconBlock` (via a dissemination of a
# `SignedBlindedBeaconBlock`) then the validator **MUST** not use the
# local build process as a fallback, even in the event of some failure
# with the external buildernetwork.
return err("unblindAndRouteBlockMEV error")

Loading

0 comments on commit 55611d7

Please sign in to comment.