query EL and builder relay for bids in parallel (#4749)

* outline for comparing bids from builder and engine API in BN

* set up async

* decision scaffold

* clean up logging

* Refactor proposeBlockMEV

* Update beacon_chain/validators/validator_duties.nim

Co-authored-by: zah <zahary@status.im>

* Update beacon_chain/validators/validator_duties.nim

Co-authored-by: zah <zahary@status.im>

* use typedescs instead of explicit generic parameters

---------

Co-authored-by: zah <zahary@status.im>
This commit is contained in:
tersec 2023-04-05 13:35:32 +00:00 committed by GitHub
parent 04302081b4
commit 3bfff6f219
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 148 additions and 95 deletions

View File

@ -85,7 +85,7 @@ declarePublicGauge(attached_validator_balance_total,
logScope: topics = "beacval"
type
ForkedBlockResult* = Result[ForkedBeaconBlock, string]
ForkedBlockResult = Result[ForkedBeaconBlock, string]
SyncStatus* {.pure.} = enum
synced
@ -392,7 +392,7 @@ proc makeBeaconBlockForHeadAndSlot*(
get_expected_withdrawals(forkyState.data))
if withdrawals_root.isNone or
hash_tree_root(withdrawals) != withdrawals_root.get:
# TODO: Why don't we fallback to the EL payload here?
# If engine API returned a block, will use that
return err("Builder relay provided incorrect withdrawals root")
# Otherwise, the state transition function notices that there are
# too few withdrawals.
@ -675,12 +675,12 @@ proc getBlindedBlockParts[EPH: ForkyExecutionPayloadHeader](
return ok((executionPayloadHeader.get, forkedBlck))
proc proposeBlockMEV[
proc getBuilderBid[
SBBB: bellatrix_mev.SignedBlindedBeaconBlock |
capella_mev.SignedBlindedBeaconBlock](
node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot,
randao: ValidatorSig, validator_index: ValidatorIndex):
Future[Opt[BlockRef]] {.async.} =
Future[Result[SBBB, string]] {.async.} =
# Used by the BN's own validators, but not the REST server
when SBBB is bellatrix_mev.SignedBlindedBeaconBlock:
type EPH = bellatrix.ExecutionPayloadHeader
@ -695,7 +695,7 @@ proc proposeBlockMEV[
if blindedBlockParts.isErr:
# Not signed yet, fine to try to fall back on EL
beacon_block_builder_missed_with_fallback.inc()
return Opt.none BlockRef
return err blindedBlockParts.error()
# These, together, get combined into the blinded block for signing and
# proposal through the relay network.
@ -730,23 +730,18 @@ proc proposeBlockMEV[
Result[SBBB, string].err("getBlindedBlock timed out")
if blindedBlock.isErr:
info "proposeBlockMEV: getBlindedBeaconBlock failed",
slot, head = shortLog(head), validator_index, blindedBlock,
error = blindedBlock.error
return Opt.none BlockRef
return err blindedBlock.error()
# Before unblindAndRouteBlockMEV, can fall back to EL; after, cannot
return blindedBlock
proc proposeBlockMEV(node: BeaconNode, blindedBlock: auto):
Future[Result[BlockRef, string]] {.async.} =
let unblindedBlockRef = await node.unblindAndRouteBlockMEV(
blindedBlock.get)
return if unblindedBlockRef.isOk and unblindedBlockRef.get.isSome:
beacon_blocks_proposed.inc()
unblindedBlockRef.get
ok(unblindedBlockRef.get.get)
else:
# Signal to the caller that a signed, blinded beacon block was sent to the
# builder API server, at which point no local EL fallback can occur. Using
# non-`none` opt with the same head indicates this to proposeBlock(), with
# any non-`none` return value indicating this in general.
#
# unblindedBlockRef.isOk and unblindedBlockRef.get.isNone indicates that
# the block failed to validate and integrate into the DAG, which for the
# purpose of this return value, is equivalent. It's used to drive Beacon
@ -756,11 +751,7 @@ proc proposeBlockMEV[
unblindedBlockRef.error
else:
"Unblinded block failed either to validate or integrate into validated store"
warn "proposeBlockMEV: blinded block either not successfully unblinded or not successfully proposed",
head = shortLog(head), slot, validator_index,
validator = shortLog(validator),
err = errMsg, blindedBlck = shortLog(blindedBlock.get)
Opt.some head
err errMsg
proc makeBlindedBeaconBlockForHeadAndSlot*[
BBB: bellatrix_mev.BlindedBeaconBlock | capella_mev.BlindedBeaconBlock](
@ -820,85 +811,104 @@ proc makeBlindedBeaconBlockForHeadAndSlot*[
else:
return err("Attempt to create pre-Bellatrix blinded block")
proc proposeBlock(node: BeaconNode,
validator: AttachedValidator,
validator_index: ValidatorIndex,
head: BlockRef,
slot: Slot): Future[BlockRef] {.async.} =
if head.slot >= slot:
# We should normally not have a head newer than the slot we're proposing for
# but this can happen if block proposal is delayed
warn "Skipping proposal, have newer head already",
headSlot = shortLog(head.slot),
headBlockRoot = shortLog(head.root),
slot = shortLog(slot)
return head
let
fork = node.dag.forkAtEpoch(slot.epoch)
genesis_validators_root = node.dag.genesis_validators_root
randao =
block:
let res = await validator.getEpochSignature(
fork, genesis_validators_root, slot.epoch)
if res.isErr():
warn "Unable to generate randao reveal",
validator = shortLog(validator), error_msg = res.error()
return head
res.get()
proc proposeBlockAux(
SBBB: typedesc, EPS: typedesc, node: BeaconNode,
validator: AttachedValidator, validator_index: ValidatorIndex,
head: BlockRef, slot: Slot, randao: ValidatorSig, fork: Fork,
genesis_validators_root: Eth2Digest): Future[BlockRef] {.async.} =
# Collect bids
let usePayloadBuilder =
if node.config.payloadBuilderEnable:
let failsafeInEffect =
withState(node.dag.headState):
# Head slot, not proposal slot, matters here
livenessFailsafeInEffect(
# TODO it might make some sense to allow use of builder API if local
# EL fails -- i.e. it would change priorities, so any block from the
# execution layer client would override builder API. But it seems an
# odd requirement to produce no block at all in those conditions.
not livenessFailsafeInEffect(
forkyState.data.block_roots.data, forkyState.data.slot)
if not failsafeInEffect:
let newBlockMEV =
if slot.epoch >= node.dag.cfg.DENEB_FORK_EPOCH:
debugRaiseAssert $denebImplementationMissing & ": proposeBlock"
await proposeBlockMEV[
capella_mev.SignedBlindedBeaconBlock](
node, head, validator, slot, randao, validator_index)
elif slot.epoch >= node.dag.cfg.CAPELLA_FORK_EPOCH:
await proposeBlockMEV[
capella_mev.SignedBlindedBeaconBlock](
node, head, validator, slot, randao, validator_index)
else:
await proposeBlockMEV[
bellatrix_mev.SignedBlindedBeaconBlock](
node, head, validator, slot, randao, validator_index)
false
if newBlockMEV.isSome:
# This might be equivalent to the `head` passed in, but it signals that
# `submitBlindedBlock` ran, so don't do anything else. Otherwise, it is
# fine to try again with the local EL.
if newBlockMEV.get == head:
# Returning same block as head indicates failure to generate new block
let
payloadBuilderBidFut =
if usePayloadBuilder:
getBuilderBid[SBBB](node, head, validator, slot, randao, validator_index)
else:
let fut = newFuture[Result[SBBB, string]]("builder-bid")
fut.complete(Result[SBBB, string].err(
"either payload builder disabled or liveness failsafe active"))
fut
engineBlockFut = makeBeaconBlockForHeadAndSlot(
EPS, node, randao, validator_index, node.graffitiBytes, head, slot)
# getBuilderBid times out after BUILDER_PROPOSAL_DELAY_TOLERANCE, with 1 more
# second for remote validators. makeBeaconBlockForHeadAndSlot times out after
# 1 second.
await allFutures(payloadBuilderBidFut, engineBlockFut)
doAssert payloadBuilderBidFut.finished and engineBlockFut.finished
let builderBidAvailable =
if payloadBuilderBidFut.completed:
if payloadBuilderBidFut.read().isOk:
true
elif usePayloadBuilder:
info "Payload builder error",
slot, head = shortLog(head), validator = shortLog(validator),
err = payloadBuilderBidFut.read().error()
false
else:
# Effectively the same case, but without the log message
false
else:
info "Payload builder bid future failed",
slot, head = shortLog(head), validator = shortLog(validator),
err = payloadBuilderBidFut.error.msg
false
let engineBidAvailable =
if engineBlockFut.completed:
if engineBlockFut.read().isOk:
true
else:
info "Engine block building error",
slot, head = shortLog(head), validator = shortLog(validator),
err = payloadBuilderBidFut.read().error()
false
else:
info "Engine block building failed",
slot, head = shortLog(head), validator = shortLog(validator),
err = engineBlockFut.error.msg
false
let useBuilderBlock =
if builderBidAvailable:
if engineBidAvailable:
true
else:
true
else:
if not engineBidAvailable:
return head # errors logged in router
false
if useBuilderBlock:
let
blindedBlock = payloadBuilderBidFut.read()
# Before proposeBlockMEV, can fall back to EL; after, cannot without
# risking slashing.
maybeUnblindedBlock = await proposeBlockMEV(node, blindedBlock)
return maybeUnblindedBlock.valueOr:
warn "Blinded block proposal incomplete",
head = shortLog(head), slot, validator_index,
validator = shortLog(validator),
err = maybeUnblindedBlock.error,
blindedBlck = shortLog(blindedBlock.get)
beacon_block_builder_missed_without_fallback.inc()
return newBlockMEV.get
return head
# TODO Compare the value of the MEV block and the execution block
# obtained from the EL below:
let newBlock =
if slot.epoch >= node.dag.cfg.DENEB_FORK_EPOCH:
await makeBeaconBlockForHeadAndSlot(
deneb.ExecutionPayloadForSigning,
node, randao, validator_index, node.graffitiBytes, head, slot)
elif slot.epoch >= node.dag.cfg.CAPELLA_FORK_EPOCH:
await makeBeaconBlockForHeadAndSlot(
capella.ExecutionPayloadForSigning,
node, randao, validator_index, node.graffitiBytes, head, slot)
else:
await makeBeaconBlockForHeadAndSlot(
bellatrix.ExecutionPayloadForSigning,
node, randao, validator_index, node.graffitiBytes, head, slot)
if newBlock.isErr():
return head # already logged elsewhere!
var forkedBlck = newBlock.get()
var forkedBlck = engineBlockFut.read().get
withBlck(forkedBlck):
var blobs_sidecar = deneb.BlobsSidecar(
@ -974,6 +984,49 @@ proc proposeBlock(node: BeaconNode,
return newBlockRef.get()
proc proposeBlock(node: BeaconNode,
validator: AttachedValidator,
validator_index: ValidatorIndex,
head: BlockRef,
slot: Slot): Future[BlockRef] {.async.} =
if head.slot >= slot:
# We should normally not have a head newer than the slot we're proposing for
# but this can happen if block proposal is delayed
warn "Skipping proposal, have newer head already",
headSlot = shortLog(head.slot),
headBlockRoot = shortLog(head.root),
slot = shortLog(slot)
return head
let
fork = node.dag.forkAtEpoch(slot.epoch)
genesis_validators_root = node.dag.genesis_validators_root
randao = block:
let res = await validator.getEpochSignature(
fork, genesis_validators_root, slot.epoch)
if res.isErr():
warn "Unable to generate randao reveal",
validator = shortLog(validator), error_msg = res.error()
return head
res.get()
template proposeBlockContinuation(type1, type2: untyped): auto =
await proposeBlockAux(
type1, type2, node, validator, validator_index, head, slot, randao, fork,
genesis_validators_root)
return
if slot.epoch >= node.dag.cfg.DENEB_FORK_EPOCH:
debugRaiseAssert $denebImplementationMissing & ": proposeBlock"
proposeBlockContinuation(
capella_mev.SignedBlindedBeaconBlock, deneb.ExecutionPayloadForSigning)
elif slot.epoch >= node.dag.cfg.CAPELLA_FORK_EPOCH:
proposeBlockContinuation(
capella_mev.SignedBlindedBeaconBlock, capella.ExecutionPayloadForSigning)
else:
proposeBlockContinuation(
bellatrix_mev.SignedBlindedBeaconBlock, bellatrix.ExecutionPayloadForSigning)
proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
## Perform all attestations that the validators attached to this node should
## perform during the given slot