Support `/eth/v1/validator/blinded_blocks` (#4272)

* Support BN endpoints for producing blinded blocks

* use correct endpoint version

* serve either JSON or SSZ versions of endpoint
This commit is contained in:
tersec 2022-10-31 17:39:03 +00:00 committed by GitHub
parent 00f083785d
commit 3ef09ff596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 215 additions and 22 deletions

View File

@ -433,6 +433,94 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
res.get()
return RestApiResponse.jsonResponsePlain(message)
# https://ethereum.github.io/beacon-APIs/#/Validator/produceBlindedBlock
# https://github.com/ethereum/beacon-APIs/blob/v2.3.0/apis/validator/blinded_block.yaml
router.api(MethodGet, "/eth/v1/validator/blinded_blocks/{slot}") do (
slot: Slot, randao_reveal: Option[ValidatorSig],
graffiti: Option[GraffitiBytes]) -> RestApiResponse:
## Requests a beacon node to produce a valid blinded block, which can then
## be signed by a validator. A blinded block is a block with only a
## transactions root, rather than a full transactions list.
##
## Metadata in the response indicates the type of block produced, and the
## supported types of block will be added to as forks progress.
let contentType =
block:
let res = preferredContentType(jsonMediaType,
sszMediaType)
if res.isErr():
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)
res.get()
let qslot = block:
if slot.isErr():
return RestApiResponse.jsonError(Http400, InvalidSlotValueError,
$slot.error())
let res = slot.get()
if res <= node.dag.finalizedHead.slot:
return RestApiResponse.jsonError(Http400, InvalidSlotValueError,
"Slot already finalized")
let
wallTime = node.beaconClock.now() + MAXIMUM_GOSSIP_CLOCK_DISPARITY
if res > wallTime.slotOrZero:
return RestApiResponse.jsonError(Http400, InvalidSlotValueError,
"Slot cannot be in the future")
res
let qrandao =
if randao_reveal.isNone():
return RestApiResponse.jsonError(Http400, MissingRandaoRevealValue)
else:
let res = randao_reveal.get()
if res.isErr():
return RestApiResponse.jsonError(Http400,
InvalidRandaoRevealValue,
$res.error())
res.get()
let qgraffiti =
if graffiti.isNone():
defaultGraffitiBytes()
else:
let res = graffiti.get()
if res.isErr():
return RestApiResponse.jsonError(Http400,
InvalidGraffitiBytesValue,
$res.error())
res.get()
let qhead =
block:
let res = node.getSyncedHead(qslot)
if res.isErr():
return RestApiResponse.jsonError(Http503, BeaconNodeInSyncError,
$res.error())
res.get()
let proposer = node.dag.getProposer(qhead, qslot)
if proposer.isNone():
return RestApiResponse.jsonError(Http400, ProposerNotFoundError)
template responsePlain(response: untyped): untyped =
if contentType == sszMediaType:
RestApiResponse.sszResponse(response)
elif contentType == jsonMediaType:
RestApiResponse.jsonResponsePlain(response)
else:
RestApiResponse.jsonError(Http500, InvalidAcceptError)
if node.currentSlot().epoch() >= node.dag.cfg.BELLATRIX_FORK_EPOCH:
let res = await makeBlindedBeaconBlockForHeadAndSlot(
node, qrandao, proposer.get(), qgraffiti, qhead, qslot)
if res.isErr():
return RestApiResponse.jsonError(Http400, res.error())
return responsePlain(ForkedBlindedBeaconBlock(
kind: BeaconBlockFork.Bellatrix,
bellatrixData: res.get()))
else:
# Pre-Bellatrix, this endpoint will return a BeaconBlock
let res = await makeBeaconBlockForHeadAndSlot(
node, qrandao, proposer.get(), qgraffiti, qhead, qslot)
if res.isErr():
return RestApiResponse.jsonError(Http400, res.error())
return responsePlain(res.get())
# https://ethereum.github.io/beacon-APIs/#/Validator/produceAttestationData
router.api(MethodGet, "/eth/v1/validator/attestation_data") do (
slot: Option[Slot],

View File

@ -1013,7 +1013,8 @@ proc readValue*[BlockType: Web3SignerForkedBeaconBlock](
kind: BeaconBlockFork.Bellatrix,
bellatrixData: res.get())
proc writeValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock](
proc writeValue*[
BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock|ForkedBlindedBeaconBlock](
writer: var JsonWriter[RestJson],
value: BlockType) {.raises: [IOError, Defect].} =

View File

@ -119,6 +119,12 @@ type
of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock
of BeaconBlockFork.Bellatrix: bellatrixData*: BeaconBlockHeader
ForkedBlindedBeaconBlock* = object
case kind*: BeaconBlockFork
of BeaconBlockFork.Phase0: phase0Data*: phase0.BeaconBlock
of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock
of BeaconBlockFork.Bellatrix: bellatrixData*: BlindedBeaconBlock
ForkedTrustedBeaconBlock* = object
case kind*: BeaconBlockFork
of BeaconBlockFork.Phase0: phase0Data*: phase0.TrustedBeaconBlock

View File

@ -90,6 +90,7 @@ logScope: topics = "beacval"
type
ForkedBlockResult* = Result[ForkedBeaconBlock, string]
BlindedBlockResult* = Result[BlindedBeaconBlock, string]
SyncStatus* {.pure.} = enum
synced
@ -523,7 +524,7 @@ proc makeBeaconBlockForHeadAndSlot*(
proc getBlindedExecutionPayload(
node: BeaconNode, slot: Slot, executionBlockRoot: Eth2Digest,
pubkey: ValidatorPubKey):
Future[Result[ExecutionPayloadHeader, cstring]] {.async.} =
Future[Result[ExecutionPayloadHeader, string]] {.async.} =
if node.payloadBuilderRestClient.isNil:
return err "getBlindedExecutionPayload: nil REST client"
@ -565,29 +566,50 @@ macro copyFields(
result.add newAssignment(
newDotExpr(dst, ident(name)), newDotExpr(src, ident(name)))
proc getBlindedBeaconBlock[T](
node: BeaconNode, slot: Slot, head: BlockRef, validator: AttachedValidator,
validator_index: ValidatorIndex, forkedBlock: ForkedBeaconBlock,
executionPayloadHeader: ExecutionPayloadHeader):
Future[Result[T, string]] {.async.} =
func constructSignableBlindedBlock[T](
forkedBlock: ForkedBeaconBlock,
executionPayloadHeader: ExecutionPayloadHeader): T =
static: doAssert high(BeaconStateFork) == BeaconStateFork.Bellatrix
const
blckFields = getFieldNames(typeof(forkedBlock.bellatrixData))
blckBodyFields = getFieldNames(typeof(forkedBlock.bellatrixData.body))
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal
var blindedBlock: T
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal
copyFields(blindedBlock.message, forkedBlock.bellatrixData, blckFields)
copyFields(
blindedBlock.message.body, forkedBlock.bellatrixData.body, blckBodyFields)
blindedBlock.message.body.execution_payload_header = executionPayloadHeader
blindedBlock
func constructPlainBlindedBlock[T](
forkedBlock: ForkedBeaconBlock,
executionPayloadHeader: ExecutionPayloadHeader): T =
static: doAssert high(BeaconStateFork) == BeaconStateFork.Bellatrix
const
blckFields = getFieldNames(typeof(forkedBlock.bellatrixData))
blckBodyFields = getFieldNames(typeof(forkedBlock.bellatrixData.body))
var blindedBlock: T
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal
copyFields(blindedBlock, forkedBlock.bellatrixData, blckFields)
copyFields(blindedBlock.body, forkedBlock.bellatrixData.body, blckBodyFields)
blindedBlock.body.execution_payload_header = executionPayloadHeader
blindedBlock
proc blindedBlockCheckSlashingAndSign[T](
node: BeaconNode, slot: Slot, validator: AttachedValidator,
validator_index: ValidatorIndex, nonsignedBlindedBlock: T):
Future[Result[T, string]] {.async.} =
# Check with slashing protection before submitBlindedBlock
let
fork = node.dag.forkAtEpoch(slot.epoch)
genesis_validators_root = node.dag.genesis_validators_root
blockRoot = hash_tree_root(blindedBlock.message)
blockRoot = hash_tree_root(nonsignedBlindedBlock.message)
signingRoot = compute_block_signing_root(
fork, genesis_validators_root, slot, blockRoot)
notSlashable = node.attachedValidators
@ -596,13 +618,14 @@ proc getBlindedBeaconBlock[T](
if notSlashable.isErr:
warn "Slashing protection activated for MEV block",
blockRoot = shortLog(blockRoot), blck = shortLog(blindedBlock),
blockRoot = shortLog(blockRoot), blck = shortLog(nonsignedBlindedBlock),
signingRoot = shortLog(signingRoot),
validator = validator.pubkey,
slot = slot,
existingProposal = notSlashable.error
return err("MEV proposal would be slashable: " & $notSlashable.error)
var blindedBlock = nonsignedBlindedBlock
blindedBlock.signature =
block:
let res = await validator.getBlockSignature(
@ -613,10 +636,20 @@ proc getBlindedBeaconBlock[T](
return ok blindedBlock
proc proposeBlockMEV(
node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot,
randao: ValidatorSig, validator_index: ValidatorIndex):
Future[Opt[BlockRef]] {.async.} =
proc getBlindedBeaconBlock[T](
node: BeaconNode, slot: Slot, validator: AttachedValidator,
validator_index: ValidatorIndex, forkedBlock: ForkedBeaconBlock,
executionPayloadHeader: ExecutionPayloadHeader):
Future[Result[T, string]] {.async.} =
return await blindedBlockCheckSlashingAndSign(
node, slot, validator, validator_index, constructSignableBlindedBlock[T](
forkedBlock, executionPayloadHeader))
proc getBlindedBlockParts(
node: BeaconNode, head: BlockRef, validator: AttachedValidator,
slot: Slot, randao: ValidatorSig, validator_index: ValidatorIndex):
Future[Result[(ExecutionPayloadHeader, ForkedBeaconBlock), string]]
{.async.} =
let
executionBlockRoot = node.dag.loadExecutionBlockRoot(head)
executionPayloadHeader =
@ -625,13 +658,13 @@ proc proposeBlockMEV(
node.getBlindedExecutionPayload(
slot, executionBlockRoot, validator.pubkey),
BUILDER_PROPOSAL_DELAY_TOLERANCE):
Result[ExecutionPayloadHeader, cstring].err(
Result[ExecutionPayloadHeader, string].err(
"getBlindedExecutionPayload timed out")
except RestDecodingError as exc:
Result[ExecutionPayloadHeader, cstring].err(
Result[ExecutionPayloadHeader, string].err(
"getBlindedExecutionPayload REST decoding error")
except CatchableError as exc:
Result[ExecutionPayloadHeader, cstring].err(
Result[ExecutionPayloadHeader, string].err(
"getBlindedExecutionPayload error")
if executionPayloadHeader.isErr:
@ -639,8 +672,7 @@ proc proposeBlockMEV(
error = executionPayloadHeader.error, slot, validator_index,
head = shortLog(head)
# Haven't committed to the MEV block, so allow EL fallback.
beacon_block_builder_missed_with_fallback.inc()
return Opt.none BlockRef
return err(executionPayloadHeader.error)
# When creating this block, need to ensure it uses the MEV-provided execution
# payload, both to avoid repeated calls to network services and to ensure the
@ -662,15 +694,32 @@ proc proposeBlockMEV(
if newBlock.isErr():
# Haven't committed to the MEV block, so allow EL fallback.
return Opt.none BlockRef # already logged elsewhere!
return err(newBlock.error) # already logged elsewhere!
let forkedBlck = newBlock.get()
return ok((executionPayloadHeader.get, forkedBlck))
proc proposeBlockMEV(
node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot,
randao: ValidatorSig, validator_index: ValidatorIndex):
Future[Opt[BlockRef]] {.async.} =
let blindedBlockParts = await getBlindedBlockParts(
node, head, validator, slot, randao, validator_index)
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
# These, together, get combined into the blinded block for signing and
# proposal through the relay network.
let (executionPayloadHeader, forkedBlck) = blindedBlockParts.get
# This is only substantively asynchronous with a remote key signer
let blindedBlock = awaitWithTimeout(
getBlindedBeaconBlock[SignedBlindedBeaconBlock](
node, slot, head, validator, validator_index, forkedBlck,
executionPayloadHeader.get),
node, slot, validator, validator_index, forkedBlck,
executionPayloadHeader),
500.milliseconds):
Result[SignedBlindedBeaconBlock, string].err "getBlindedBlock timed out"
@ -761,6 +810,45 @@ proc proposeBlockMEV(
error = blindedBlock.error
return Opt.none BlockRef
proc makeBlindedBeaconBlockForHeadAndSlot*(
node: BeaconNode, randao_reveal: ValidatorSig,
validator_index: ValidatorIndex, graffiti: GraffitiBytes, head: BlockRef,
slot: Slot): Future[BlindedBlockResult] {.async.} =
## Requests a beacon node to produce a valid blinded block, which can then be
## signed by a validator. A blinded block is a block with only a transactions
## root, rather than a full transactions list.
let
validator =
# Relevant state for knowledge of validators
withState(node.dag.headState):
if distinctBase(validator_index) >= forkyState.data.validators.lenu64:
debug "makeBlindedBeaconBlockForHeadAndSlot: invalid validator index",
head = shortLog(head),
validator_index,
validators_len = forkyState.data.validators.len
return err("Invalid validator index")
let pubkey = forkyState.data.validators.item(validator_index).pubkey
if pubkey notin node.attachedValidators.validators:
debug "makeBlindedBeaconBlockForHeadAndSlot: validator pubkey unknown",
head = shortLog(head),
validator_index,
validators_len = forkyState.data.validators.len,
pubkey = shortLog(pubkey)
return err("Validator pubkey unknown")
node.attachedValidators.validators[pubkey]
blindedBlockParts = await getBlindedBlockParts(
node, head, validator, slot, randao_reveal, validator_index)
if blindedBlockParts.isErr:
# Don't try EL fallback -- VC specifically requested a blinded block
return err("Unable to create blinded block")
let (executionPayloadHeader, forkedBlck) = blindedBlockParts.get
return ok constructPlainBlindedBlock[BlindedBeaconBlock](
forkedBlck, executionPayloadHeader)
proc proposeBlock(node: BeaconNode,
validator: AttachedValidator,
validator_index: ValidatorIndex,

View File

@ -3097,6 +3097,16 @@
"status": {"operator": "equals", "value": "400"}
}
},
{
"topics": ["validator", "blinded_blocks"],
"request": {
"url": "/eth/v1/validator/blinded_blocks/0",
"headers": {"Accept": "application/json"}
},
"response": {
"status": {"operator": "equals", "value": "400"}
}
},
{
"topics": ["validator", "attestation_data"],
"request": {