nimbus-eth2/beacon_chain/validators/message_router_mev.nim

203 lines
8.7 KiB
Nim

# beacon_chain
# Copyright (c) 2022-2023 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.
{.push raises: [].}
import std/macros
import metrics
import stew/assign2
import ../beacon_node
from eth/async_utils import awaitWithTimeout
from ../spec/datatypes/bellatrix import SignedBeaconBlock
from ../spec/mev/rest_capella_mev_calls import submitBlindedBlock
from ../spec/mev/rest_deneb_mev_calls import submitBlindedBlock
const
BUILDER_BLOCK_SUBMISSION_DELAY_TOLERANCE = 5.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", "withdrawals_root"]:
# TODO use stew/assign2
result.add newAssignment(
newDotExpr(dst, ident(name)), newDotExpr(src, ident(name)))
# TODO when https://github.com/nim-lang/Nim/issues/21346 and/or
# https://github.com/nim-lang/Nim/issues/21347 fixed, combine and make generic
# these two very similar versions of unblindAndRouteBlockMEV
proc unblindAndRouteBlockMEV*(
node: BeaconNode, payloadBuilderRestClient: RestClientRef,
blindedBlock: capella_mev.SignedBlindedBeaconBlock):
Future[Result[Opt[BlockRef], string]] {.async.} =
info "Proposing blinded Builder API block",
blindedBlock = shortLog(blindedBlock)
# By time submitBlindedBlock is called, must already have done slashing
# protection check
let unblindedPayload =
try:
awaitWithTimeout(
payloadBuilderRestClient.submitBlindedBlock(blindedBlock),
BUILDER_BLOCK_SUBMISSION_DELAY_TOLERANCE):
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:
return err("REST decoding error submitting blinded block: " & exc.msg)
except CatchableError as exc:
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):
return err("unblinded payload doesn't match blinded payload header: " &
$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.3.0/specs/bellatrix/validator.md#block-proposal
var signedBlock = capella.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
signedBlock.root = hash_tree_root(signedBlock.message)
doAssert signedBlock.root == hash_tree_root(blindedBlock.message)
debug "unblindAndRouteBlockMEV: proposing unblinded block",
blck = shortLog(signedBlock)
let newBlockRef =
(await node.router.routeSignedBeaconBlock(
signedBlock, Opt.none(SignedBlobSidecars))).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:
return err("submitBlindedBlock failed with HTTP error code" &
$unblindedPayload.status & ": " & $shortLog(blindedBlock))
# https://github.com/ethereum/builder-specs/blob/v0.3.0/specs/bellatrix/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 builder network.
return err("unblindAndRouteBlockMEV error")
# TODO currently cannot be combined into one generic function
proc unblindAndRouteBlockMEV*(
node: BeaconNode, payloadBuilderRestClient: RestClientRef,
blindedBlockContents: deneb_mev.SignedBlindedBeaconBlockContents):
Future[Result[Opt[BlockRef], string]] {.async.} =
template blindedBlock: untyped = blindedBlockContents.signed_blinded_block
info "Proposing blinded Builder API block and blobs",
blindedBlock = shortLog(blindedBlock)
# By time submitBlindedBlock is called, must already have done slashing
# protection check
let unblindedPayload =
try:
awaitWithTimeout(
payloadBuilderRestClient.submitBlindedBlock(blindedBlockContents),
BUILDER_BLOCK_SUBMISSION_DELAY_TOLERANCE):
return err("Submitting blinded block and blobs 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:
return err("REST decoding error submitting blinded block and blobs: " & exc.msg)
except CatchableError as exc:
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.execution_payload):
return err("unblinded payload doesn't match blinded payload header: " &
$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.3.0/specs/bellatrix/validator.md#block-proposal
var signedBlock = deneb.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)))
assign(
signedBlock.message.body.execution_payload,
unblindedPayload.data.data.execution_payload)
signedBlock.root = hash_tree_root(signedBlock.message)
doAssert signedBlock.root == hash_tree_root(blindedBlock.message)
debug "unblindAndRouteBlockMEV: proposing unblinded block and blobs",
blck = shortLog(signedBlock)
let newBlockRef =
(await node.router.routeSignedBeaconBlock(
signedBlock, Opt.none(SignedBlobSidecars))).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)
discard $denebImplementationMissing & ": route unblinded blobs"
return ok newBlockRef
else:
return err("submitBlindedBlock failed with HTTP error code" &
$unblindedPayload.status & ": " & $shortLog(blindedBlock))
# https://github.com/ethereum/builder-specs/blob/v0.3.0/specs/bellatrix/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 builder network.